blob: 442973142ca8983e3e40889d4f48af7b032f747e [file] [log] [blame]
# Copyright 2014-2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
import os
import portage
portage._internal_caller = True
portage._sync_mode = True
from portage.output import bold, red, create_color_func
from portage._global_updates import _global_updates
from portage.sync.controller import SyncManager
from portage.util.digraph import digraph
from portage.util.futures import asyncio
from portage.util._async.AsyncScheduler import AsyncScheduler
import _emerge
from _emerge.emergelog import emergelog
portage.proxy.lazyimport.lazyimport(globals(),
'_emerge.actions:adjust_configs,load_emerge_config',
'_emerge.chk_updated_cfg_files:chk_updated_cfg_files',
'_emerge.main:parse_opts',
'_emerge.post_emerge:display_news_notification',
)
warn = create_color_func("WARN")
class SyncRepos:
short_desc = "Check repos.conf settings and/or sync repositories"
@staticmethod
def name():
return "sync"
def can_progressbar(self, func):
return False
def __init__(self, emerge_config=None, emerge_logging=False):
'''Class init function
@param emerge_config: optional an emerge_config instance to use
@param emerge_logging: boolean, defaults to False
'''
if emerge_config is None:
# need a basic options instance
actions, opts, _files = parse_opts([], silent=True)
emerge_config = load_emerge_config(
action='sync', args=_files, opts=opts)
# Parse EMERGE_DEFAULT_OPTS, for settings like
# --package-moves=n.
cmdline = portage.util.shlex_split(
emerge_config.target_config.settings.get(
"EMERGE_DEFAULT_OPTS", ""))
emerge_config.opts = parse_opts(cmdline, silent=True)[1]
if hasattr(portage, 'settings'):
# cleanly destroy global objects
portage._reset_legacy_globals()
# update redundant global variables, for consistency
# and in order to conserve memory
portage.settings = emerge_config.target_config.settings
portage.db = emerge_config.trees
portage.root = portage.db._target_eroot
self.emerge_config = emerge_config
if emerge_logging:
_emerge.emergelog._disable = False
self.xterm_titles = "notitles" not in \
self.emerge_config.target_config.settings.features
emergelog(self.xterm_titles, " === sync")
def auto_sync(self, **kwargs):
'''Sync auto-sync enabled repos'''
options = kwargs.get('options', None)
if options:
return_messages = options.get('return-messages', False)
else:
return_messages = False
success, repos, msgs = self._get_repos(auto_sync_only=True)
if not success:
if return_messages:
return (False, msgs)
return (False, None)
return self._sync(repos, return_messages, emaint_opts=options)
def all_repos(self, **kwargs):
'''Sync all repos defined in repos.conf'''
options = kwargs.get('options', None)
if options:
return_messages = options.get('return-messages', False)
else:
return_messages = False
success, repos, msgs = self._get_repos(auto_sync_only=False)
if not success:
if return_messages:
return (False, msgs)
return (False, None)
return self._sync(repos, return_messages, emaint_opts=options)
def repo(self, **kwargs):
'''Sync the specified repo'''
options = kwargs.get('options', None)
if options:
repo_names = options.get('repo', '')
return_messages = options.get('return-messages', False)
else:
return_messages = False
if isinstance(repo_names, str):
repo_names = repo_names.split()
success, repos, msgs = self._get_repos(auto_sync_only=False,
match_repos=repo_names)
if not success:
if return_messages:
return (False, msgs)
return (False, None)
return self._sync(repos, return_messages, emaint_opts=options)
@staticmethod
def _match_repos(repos, available):
'''Internal search, matches up the repo name or alias in repos.
@param repos: list of repo names or aliases to match
@param avalable: list of repo objects to search
@return: list of repo objects that match
'''
selected = []
for repo in available:
if repo.name in repos:
selected.append(repo)
elif (repo.aliases is not None and
any(alias in repos for alias in repo.aliases)):
selected.append(repo)
return selected
def _get_repos(self, auto_sync_only=True, match_repos=None):
msgs = []
repos = self.emerge_config.target_config.settings.repositories
if match_repos is not None:
# Discard duplicate repository names or aliases.
match_repos = set(match_repos)
repos = self._match_repos(match_repos, repos)
if len(repos) < len(match_repos):
# Build a set of all the matched repos' names and aliases so we
# can do a set difference for names that are missing.
repo_names = set()
for repo in repos:
repo_names.add(repo.name)
if repo.aliases is not None:
repo_names.update(repo.aliases)
missing = match_repos - repo_names
if missing:
msgs.append(red(" * ") + "The specified repo(s) were not found: %s" %
(" ".join(repo_name for repo_name in missing)) + \
"\n ...returning")
return (False, repos, msgs)
if auto_sync_only:
repos = self._filter_auto(repos)
sync_disabled = [repo for repo in repos if repo.sync_type is None]
if sync_disabled:
repos = [repo for repo in repos if repo.sync_type is not None]
if match_repos is not None:
msgs.append(red(" * " ) + "The specified repo(s) have sync disabled: %s" %
" ".join(repo.name for repo in sync_disabled) + \
"\n ...returning")
return (False, repos, msgs)
missing_sync_uri = [repo for repo in repos if repo.sync_uri is None]
if missing_sync_uri:
repos = [repo for repo in repos if repo.sync_uri is not None]
msgs.append(red(" * ") + "The specified repo(s) are missing sync-uri: %s" %
" ".join(repo.name for repo in missing_sync_uri) + \
"\n ...returning")
return (False, repos, msgs)
return (True, repos, msgs)
def _filter_auto(self, repos):
selected = []
for repo in repos:
if repo.auto_sync in ['yes', 'true']:
selected.append(repo)
return selected
def _sync(self, selected_repos, return_messages, emaint_opts=None):
msgs = []
if not selected_repos:
if return_messages:
msgs.append("Nothing to sync... returning")
return (True, msgs)
return (True, None)
if emaint_opts is not None:
for k, v in emaint_opts.items():
if v is not None:
k = "--" + k.replace("_", "-")
self.emerge_config.opts[k] = v
# Portage needs to ensure a sane umask for the files it creates.
os.umask(0o22)
sync_manager = SyncManager(
self.emerge_config.target_config.settings, emergelog)
max_jobs = (self.emerge_config.opts.get('--jobs', 1)
if 'parallel-fetch' in self.emerge_config.
target_config.settings.features else 1)
sync_scheduler = SyncScheduler(emerge_config=self.emerge_config,
selected_repos=selected_repos, sync_manager=sync_manager,
max_jobs=max_jobs,
event_loop=asyncio._safe_loop())
sync_scheduler.start()
sync_scheduler.wait()
retvals = sync_scheduler.retvals
msgs.extend(sync_scheduler.msgs)
returncode = True
if retvals:
msgs.extend(self.rmessage(retvals, 'sync'))
for repo, retval in retvals:
if retval != os.EX_OK:
returncode = False
break
else:
msgs.extend(self.rmessage([('None', os.EX_OK)], 'sync'))
# run the post_sync_hook one last time for
# run only at sync completion hooks
if sync_scheduler.global_hooks_enabled:
rcode = sync_manager.perform_post_sync_hook('')
if rcode:
msgs.extend(self.rmessage([('None', rcode)], 'post-sync'))
if rcode != os.EX_OK:
returncode = False
# Reload the whole config.
portage._sync_mode = False
self._reload_config()
self._do_pkg_moves()
msgs.extend(self._check_updates())
display_news_notification(self.emerge_config.target_config,
self.emerge_config.opts)
if return_messages:
return (returncode, msgs)
return (returncode, None)
def _do_pkg_moves(self):
if self.emerge_config.opts.get('--package-moves') != 'n' and \
_global_updates(self.emerge_config.trees,
self.emerge_config.target_config.mtimedb["updates"],
quiet=("--quiet" in self.emerge_config.opts)):
self.emerge_config.target_config.mtimedb.commit()
# Reload the whole config.
self._reload_config()
def _check_updates(self):
mybestpv = self.emerge_config.target_config.trees['porttree'].dbapi.xmatch(
"bestmatch-visible", portage.const.PORTAGE_PACKAGE_ATOM)
mypvs = portage.best(
self.emerge_config.target_config.trees['vartree'].dbapi.match(
portage.const.PORTAGE_PACKAGE_ATOM))
try:
old_use = (
self.emerge_config.target_config.trees["vartree"]
.dbapi.aux_get(mypvs, ["USE"])[0]
.split()
)
except KeyError:
old_use = ()
chk_updated_cfg_files(
self.emerge_config.target_config.root,
portage.util.shlex_split(
self.emerge_config.target_config.settings.get("CONFIG_PROTECT", "")
),
)
msgs = []
if not (mybestpv and mypvs) or mybestpv == mypvs or "--quiet" in self.emerge_config.opts:
return msgs
# Suggest to update to the latest available version of portage.
# Since changes to PYTHON_TARGETS cause complications, this message
# is suppressed if the new version has different PYTHON_TARGETS enabled
# than previous version.
portdb = self.emerge_config.target_config.trees["porttree"].dbapi
portdb.doebuild_settings.setcpv(mybestpv, mydb=portdb)
usemask = portdb.doebuild_settings.usemask
useforce = portdb.doebuild_settings.useforce
new_use = (
frozenset(portdb.doebuild_settings["PORTAGE_USE"].split()) | useforce
) - usemask
new_python_targets = frozenset(
x for x in new_use if x.startswith("python_targets_")
)
old_python_targets = frozenset(
x for x in old_use if x.startswith("python_targets_")
)
if new_python_targets == old_python_targets:
msgs.append('')
msgs.append(warn(" * ")+bold("An update to portage is available.")+" It is _highly_ recommended")
msgs.append(warn(" * ")+"that you update portage now, before any other packages are updated.")
msgs.append('')
msgs.append(warn(" * ")+"To update portage, run 'emerge --oneshot sys-apps/portage' now.")
msgs.append('')
return msgs
def _reload_config(self):
'''Reload the whole config from scratch.'''
load_emerge_config(emerge_config=self.emerge_config)
adjust_configs(self.emerge_config.opts, self.emerge_config.trees)
def rmessage(self, rvals, action):
'''Creates emaint style messages to return to the task handler'''
messages = []
for rval in rvals:
messages.append("Action: %s for repo: %s, returned code = %s"
% (action, rval[0], rval[1]))
return messages
class SyncScheduler(AsyncScheduler):
'''
Sync repos in parallel, but don't sync a given repo until all
of its masters have synced.
'''
def __init__(self, **kwargs):
'''
@param emerge_config: an emerge_config instance
@param selected_repos: list of RepoConfig instances
@param sync_manager: a SyncManger instance
'''
self._emerge_config = kwargs.pop('emerge_config')
self._selected_repos = kwargs.pop('selected_repos')
self._sync_manager = kwargs.pop('sync_manager')
AsyncScheduler.__init__(self, **kwargs)
self._init_graph()
self.retvals = []
self.msgs = []
def _init_graph(self):
'''
Graph relationships between repos and their masters.
'''
self._sync_graph = digraph()
self._leaf_nodes = []
self._repo_map = {}
self._running_repos = set()
selected_repo_names = frozenset(repo.name
for repo in self._selected_repos)
for repo in self._selected_repos:
self._repo_map[repo.name] = repo
self._sync_graph.add(repo.name, None)
for master in repo.masters:
if master.name in selected_repo_names:
self._repo_map[master.name] = master
self._sync_graph.add(master.name, repo.name)
self._complete_graph = self._sync_graph.copy()
self._hooks_repos = set()
self._update_leaf_nodes()
def _task_exit(self, task):
'''
Remove the task from the graph, in order to expose
more leaf nodes.
'''
self._running_tasks.discard(task)
# Set hooks_enabled = True by default, in order to ensure
# that hooks will be called in a backward-compatible manner
# even if all sync tasks have failed.
hooks_enabled = True
returncode = task.returncode
if task.returncode == os.EX_OK:
returncode, message, updatecache_flg, hooks_enabled = task.result
if message:
self.msgs.append(message)
repo = task.kwargs['repo'].name
self._running_repos.remove(repo)
self.retvals.append((repo, returncode))
self._sync_graph.remove(repo)
self._update_leaf_nodes()
if hooks_enabled:
self._hooks_repos.add(repo)
super(SyncScheduler, self)._task_exit(self)
def _master_hooks(self, repo_name):
"""
@param repo_name: a repo name
@type repo_name: str
@return: True if hooks would have been executed for any master
repositories of the given repo, False otherwise
@rtype: bool
"""
traversed_nodes = set()
node_stack = [repo_name]
while node_stack:
node = node_stack.pop()
if node in self._hooks_repos:
return True
if node not in traversed_nodes:
traversed_nodes.add(node)
node_stack.extend(self._complete_graph.child_nodes(node))
return False
@property
def global_hooks_enabled(self):
"""
@return: True if repo.postsync.d hooks would have been executed
for any repositories.
@rtype: bool
"""
return bool(self._hooks_repos)
def _update_leaf_nodes(self):
'''
Populate self._leaf_nodes with current leaves from
self._sync_graph. If a circular master relationship
is discovered, choose a random node to break the cycle.
'''
if self._sync_graph and not self._leaf_nodes:
self._leaf_nodes = [obj for obj in
self._sync_graph.leaf_nodes()
if obj not in self._running_repos]
if not (self._leaf_nodes or self._running_repos):
# If there is a circular master relationship,
# choose a random node to break the cycle.
self._leaf_nodes = [next(iter(self._sync_graph))]
def _next_task(self):
'''
Return a task for the next available leaf node.
'''
if not self._sync_graph:
raise StopIteration()
# If self._sync_graph is non-empty, then self._leaf_nodes
# is guaranteed to be non-empty, since otherwise
# _can_add_job would have returned False and prevented
# _next_task from being immediately called.
node = self._leaf_nodes.pop()
self._running_repos.add(node)
self._update_leaf_nodes()
return self._sync_manager.sync_async(
emerge_config=self._emerge_config,
repo=self._repo_map[node],
master_hooks=self._master_hooks(node))
def _can_add_job(self):
'''
Returns False if there are no leaf nodes available.
'''
if not AsyncScheduler._can_add_job(self):
return False
return bool(self._leaf_nodes) and not self._terminated.is_set()
def _keep_scheduling(self):
'''
Schedule as long as the graph is non-empty, and we haven't
been terminated.
'''
return bool(self._sync_graph) and not self._terminated.is_set()