Implement --autounmask-write
authorSebastian Luther <SebastianLuther@gmx.de>
Sun, 15 May 2011 19:01:03 +0000 (12:01 -0700)
committerZac Medico <zmedico@gentoo.org>
Sun, 15 May 2011 19:01:03 +0000 (12:01 -0700)
Enabling this option together with --autounmask writes proposed
changes to config files, honoring CONFIG_PROTECT.

man/emerge.1
pym/_emerge/depgraph.py
pym/_emerge/help.py
pym/_emerge/main.py

index 539b2e39ffa251aff52ad30a6511921ae52e7d84..0f2acc1d12e7e783de9e56932077b90b0204da65 100644 (file)
@@ -301,6 +301,10 @@ the specified configuration file(s). Currently,
 this only works for unstable KEYWORDS masks,
 LICENSE masks, and package.use settings.
 .TP
+.BR "\-\-autounmask\-write [ y | n ]"
+If \-\-autounmask is enabled, changes are written
+to config files, respecting \fBCONFIG_PROTECT\fR.
+.TP
 .BR \-\-backtrack=COUNT
 Specifies an integer number of times to backtrack if
 dependency calculation fails due to a conflict or an
index 16cb7fcbbded4e44c4ad2196df79c479642d0f7c..8558436bb44acd3eb3b3a29f9c7fcc86437e1d10 100644 (file)
@@ -3,6 +3,7 @@
 
 from __future__ import print_function
 
+import codecs
 import difflib
 import gc
 import logging
@@ -14,12 +15,12 @@ from itertools import chain
 
 import portage
 from portage import os, OrderedDict
-from portage import _unicode_decode
-from portage.const import PORTAGE_PACKAGE_ATOM
+from portage import _unicode_decode, _unicode_encode, _encodings
+from portage.const import PORTAGE_PACKAGE_ATOM, USER_CONFIG_PATH
 from portage.dbapi import dbapi
 from portage.dep import Atom, extract_affecting_use, check_required_use, human_readable_required_use, _repo_separator
 from portage.eapi import eapi_has_strong_blocks, eapi_has_required_use
-from portage.exception import InvalidAtom, InvalidDependString
+from portage.exception import InvalidAtom, InvalidDependString, PortageException
 from portage.output import colorize, create_color_func, \
        darkgreen, green
 bad = create_color_func("BAD")
@@ -27,8 +28,9 @@ from portage.package.ebuild.getmaskingstatus import \
        _getmaskingstatus, _MaskReason
 from portage._sets import SETPREFIX
 from portage._sets.base import InternalPackageSet
+from portage.util import ConfigProtect, shlex_split, new_protect_filename
 from portage.util import cmp_sort_key, writemsg, writemsg_stdout
-from portage.util import writemsg_level
+from portage.util import writemsg_level, write_atomic
 from portage.util.digraph import digraph
 from portage.versions import catpkgsplit
 
@@ -5493,55 +5495,14 @@ class depgraph(object):
 
                return display(self, mylist, favorites, verbosity)
 
-       def display_problems(self):
+       def _display_autounmask(self):
                """
-               Display problems with the dependency graph such as slot collisions.
-               This is called internally by display() to show the problems _after_
-               the merge list where it is most likely to be seen, but if display()
-               is not going to be called then this method should be called explicitly
-               to ensure that the user is notified of problems with the graph.
-
-               All output goes to stderr, except for unsatisfied dependencies which
-               go to stdout for parsing by programs such as autounmask.
+               Display --autounmask message and optionally write them to config files
+               (using CONFIG_PROTECT). The message includes the comments and the changes.
                """
 
-               # Note that show_masked_packages() sends it's output to
-               # stdout, and some programs such as autounmask parse the
-               # output in cases when emerge bails out. However, when
-               # show_masked_packages() is called for installed packages
-               # here, the message is a warning that is more appropriate
-               # to send to stderr, so temporarily redirect stdout to
-               # stderr. TODO: Fix output code so there's a cleaner way
-               # to redirect everything to stderr.
-               sys.stdout.flush()
-               sys.stderr.flush()
-               stdout = sys.stdout
-               try:
-                       sys.stdout = sys.stderr
-                       self._display_problems()
-               finally:
-                       sys.stdout = stdout
-                       sys.stdout.flush()
-                       sys.stderr.flush()
-
-               # This goes to stdout for parsing by programs like autounmask.
-               for pargs, kwargs in self._dynamic_config._unsatisfied_deps_for_display:
-                       self._show_unsatisfied_dep(*pargs, **kwargs)
-
-       def _display_problems(self):
-               if self._dynamic_config._circular_deps_for_display is not None:
-                       self._show_circular_deps(
-                               self._dynamic_config._circular_deps_for_display)
-
-               # The user is only notified of a slot conflict if
-               # there are no unresolvable blocker conflicts.
-               if self._dynamic_config._unsatisfied_blockers_for_display is not None:
-                       self._show_unsatisfied_blockers(
-                               self._dynamic_config._unsatisfied_blockers_for_display)
-               elif self._dynamic_config._slot_collision_info:
-                       self._show_slot_collision_notice()
-               else:
-                       self._show_missed_update()
+               autounmask_write = self._frozen_config.myopts.get("--autounmask-write", "n") == True
+               pretend = "--pretend" in self._frozen_config.myopts
 
                def check_if_latest(pkg):
                        is_latest = True
@@ -5569,11 +5530,16 @@ class depgraph(object):
 
                        return is_latest, is_latest_in_slot
 
+               #Set of roots we have autounmask changes for.
+               roots = set()
 
-               unstable_keyword_msg = []
+               unstable_keyword_msg = {}
                for pkg in self._dynamic_config._needed_unstable_keywords:
                        self._show_merge_list()
                        if pkg in self._dynamic_config.digraph:
+                               root = pkg.root
+                               roots.add(root)
+                               unstable_keyword_msg.setdefault(root, [])
                                is_latest, is_latest_in_slot = check_if_latest(pkg)
                                pkgsettings = self._frozen_config.pkgsettings[pkg.root]
                                mreasons = _get_masking_status(pkg, pkgsettings, pkg.root_config,
@@ -5583,18 +5549,21 @@ class depgraph(object):
                                                reason.unmask_hint.key == 'unstable keyword':
                                                keyword = reason.unmask_hint.value
 
-                                               unstable_keyword_msg.append(self._get_dep_chain_as_comment(pkg))
+                                               unstable_keyword_msg[root].append(self._get_dep_chain_as_comment(pkg))
                                                if is_latest:
-                                                       unstable_keyword_msg.append(">=%s %s\n" % (pkg.cpv, keyword))
+                                                       unstable_keyword_msg[root].append(">=%s %s\n" % (pkg.cpv, keyword))
                                                elif is_latest_in_slot:
-                                                       unstable_keyword_msg.append(">=%s:%s %s\n" % (pkg.cpv, pkg.metadata["SLOT"], keyword))
+                                                       unstable_keyword_msg[root].append(">=%s:%s %s\n" % (pkg.cpv, pkg.metadata["SLOT"], keyword))
                                                else:
-                                                       unstable_keyword_msg.append("=%s %s\n" % (pkg.cpv, keyword))
+                                                       unstable_keyword_msg[root].append("=%s %s\n" % (pkg.cpv, keyword))
 
-               use_changes_msg = []
+               use_changes_msg = {}
                for pkg, needed_use_config_change in self._dynamic_config._needed_use_config_changes.items():
                        self._show_merge_list()
                        if pkg in self._dynamic_config.digraph:
+                               root = pkg.root
+                               roots.add(root)
+                               use_changes_msg.setdefault(root, [])
                                is_latest, is_latest_in_slot = check_if_latest(pkg)
                                changes = needed_use_config_change[1]
                                adjustments = []
@@ -5603,42 +5572,192 @@ class depgraph(object):
                                                adjustments.append(flag)
                                        else:
                                                adjustments.append("-" + flag)
-                               use_changes_msg.append(self._get_dep_chain_as_comment(pkg, unsatisfied_dependency=True))
+                               use_changes_msg[root].append(self._get_dep_chain_as_comment(pkg, unsatisfied_dependency=True))
                                if is_latest:
-                                       use_changes_msg.append(">=%s %s\n" % (pkg.cpv, " ".join(adjustments)))
+                                       use_changes_msg[root].append(">=%s %s\n" % (pkg.cpv, " ".join(adjustments)))
                                elif is_latest_in_slot:
-                                       use_changes_msg.append(">=%s:%s %s\n" % (pkg.cpv, pkg.metadata["SLOT"], " ".join(adjustments)))
+                                       use_changes_msg[root].append(">=%s:%s %s\n" % (pkg.cpv, pkg.metadata["SLOT"], " ".join(adjustments)))
                                else:
-                                       use_changes_msg.append("=%s %s\n" % (pkg.cpv, " ".join(adjustments)))
+                                       use_changes_msg[root].append("=%s %s\n" % (pkg.cpv, " ".join(adjustments)))
 
-               license_msg = []
+               license_msg = {}
                for pkg, missing_licenses in self._dynamic_config._needed_license_changes.items():
                        self._show_merge_list()
                        if pkg in self._dynamic_config.digraph:
+                               root = pkg.root
+                               roots.add(root)
+                               license_msg.setdefault(root, [])
                                is_latest, is_latest_in_slot = check_if_latest(pkg)
 
-                               license_msg.append(self._get_dep_chain_as_comment(pkg))
+                               license_msg[root].append(self._get_dep_chain_as_comment(pkg))
                                if is_latest:
-                                       license_msg.append(">=%s %s\n" % (pkg.cpv, " ".join(sorted(missing_licenses))))
+                                       license_msg[root].append(">=%s %s\n" % (pkg.cpv, " ".join(sorted(missing_licenses))))
                                elif is_latest_in_slot:
-                                       license_msg.append(">=%s:%s %s\n" % (pkg.cpv, pkg.metadata["SLOT"], " ".join(sorted(missing_licenses))))
+                                       license_msg[root].append(">=%s:%s %s\n" % (pkg.cpv, pkg.metadata["SLOT"], " ".join(sorted(missing_licenses))))
                                else:
-                                       license_msg.append("=%s %s\n" % (pkg.cpv, " ".join(sorted(missing_licenses))))
+                                       license_msg[root].append("=%s %s\n" % (pkg.cpv, " ".join(sorted(missing_licenses))))
 
-               if unstable_keyword_msg:
-                       writemsg_stdout("\nThe following " + colorize("BAD", "keyword changes") + \
-                               " are necessary to proceed:\n", noiselevel=-1)
-                       writemsg_stdout("".join(unstable_keyword_msg), noiselevel=-1)
+               def find_config_file(abs_user_config, file_name):
+                       """
+                       Searches /etc/portage for an appropiate file to append changes to.
+                       If the file_name is a file it is returned, if it is a directoy, the
+                       last file in it is returned.
 
-               if use_changes_msg:
-                       writemsg_stdout("\nThe following " + colorize("BAD", "USE changes") + \
-                               " are necessary to proceed:\n", noiselevel=-1)
-                       writemsg_stdout("".join(use_changes_msg), noiselevel=-1)
+                       file_name - String containg a file name like "package.use"
+                       return value - String. Absolte path of file to write to. None if
+                       no suitable file exists.
+                       """
+                       file_path = os.path.join(abs_user_config, file_name)
+                       if os.path.exists(file_path):
+                               if os.path.isfile(file_path):
+                                       return file_path
+                               elif os.path.isdir(file_path):
+                                       try:
+                                               files = sorted(f for f in os.listdir(file_path) \
+                                                       if os.path.isfile(os.path.join(file_path, f)))
+                                               if len(files) != 0:
+                                                       return  os.path.join(file_path, files[-1])
+                                       except OSError:
+                                               pass
+
+
+               write_to_file = autounmask_write and not pretend
+               #Make sure we have a file to write to before doing any write.
+               file_to_write_to = {}
+               problems = []
+               if write_to_file:
+                       for root in roots:
+                               abs_user_config = os.path.join(root, USER_CONFIG_PATH)
+
+                               if root in unstable_keyword_msg:
+                                       file_to_write_to[(abs_user_config, "package.keywords")] = \
+                                               find_config_file(abs_user_config, "package.keywords")
 
-               if license_msg:
-                       writemsg_stdout("\nThe following " + colorize("BAD", "license changes") + \
+                               if root in use_changes_msg:
+                                       file_to_write_to[(abs_user_config, "package.use")] = \
+                                               find_config_file(abs_user_config, "package.use")
+
+                               if root in license_msg:
+                                       file_to_write_to[(abs_user_config, "package.license")] = \
+                                               find_config_file(abs_user_config, "package.license")
+
+                       for (abs_user_config, f), path in file_to_write_to.items():
+                               if path is None:
+                                       problems.append("!!! No file to write for '%s'\n" % os.path.join(abs_user_config, f))
+
+                       write_to_file = not problems
+
+
+               protect_obj = {}
+               if write_to_file:
+                       for root in roots:
+                               settings = self._frozen_config.pkgsettings[root]
+                               protect_obj[root] = ConfigProtect(root, \
+                                       shlex_split(settings.get("CONFIG_PROTECT", "")),
+                                       shlex_split(settings.get("CONFIG_PROTECT_MASK", "")))
+
+               def write_changes(root, change_type, changes, file_to_write_to):
+                       writemsg_stdout("\nThe following " + colorize("BAD", "%s changes" % change_type) + \
                                " are necessary to proceed:\n", noiselevel=-1)
-                       writemsg_stdout("".join(license_msg), noiselevel=-1)
+                       writemsg_stdout("".join(changes), noiselevel=-1)
+                       if write_to_file:
+                               try:
+                                       file_contents = codecs.open(
+                                               _unicode_encode(file_to_write_to,
+                                               encoding=_encodings['fs'], errors='strict'),
+                                               mode='r', encoding=_encodings['content'],
+                                               errors='replace').readlines()
+                               except IOError as e:
+                                       problems.append("!!! Failed to read '%s': %s\n" % (file_to_write_to, e))
+                               else:
+                                       file_contents.extend(changes)
+                                       if protect_obj[root].isprotected(file_to_write_to):
+                                               file_to_write_to = new_protect_filename(file_to_write_to)
+                                       try:
+                                               write_atomic(file_to_write_to, "".join(file_contents))
+                                       except PortageException:
+                                               problems.append("!!! Failed to write '%s'\n" % file_to_write_to)
+
+               for root in roots:
+                       abs_user_config = os.path.join(root, USER_CONFIG_PATH)
+                       if len(roots) > 1:
+                               writemsg_stdout("\nFor %s:\n" % abs_user_config, noiselevel=-1)
+
+                       if root in unstable_keyword_msg:
+                               write_changes(root, "keyword", unstable_keyword_msg[root],
+                                       file_to_write_to.get((abs_user_config, "package.keywords")))
+
+                       if root in use_changes_msg:
+                               write_changes(root, "USE", use_changes_msg[root],
+                                       file_to_write_to.get((abs_user_config, "package.use")))
+
+                       if root in license_msg:
+                               write_changes(root, "license", license_msg[root],
+                                       file_to_write_to.get((abs_user_config, "package.license")))
+
+               if problems:
+                       writemsg_stdout("\nThe following problems occured while writing autounmask changes:\n", \
+                               noiselevel=-1)
+                       writemsg_stdout("".join(problems), noiselevel=-1)
+               elif write_to_file and roots:
+                       writemsg_stdout("\nAutounmask changes successfully written. Remeber to run etc-update.\n", \
+                               noiselevel=-1)
+               elif not pretend and not autounmask_write and roots:
+                       writemsg_stdout("\nUse --autounmask-write to write changes to config files (honoring CONFIG_PROTECT).\n", \
+                               noiselevel=-1)
+
+
+       def display_problems(self):
+               """
+               Display problems with the dependency graph such as slot collisions.
+               This is called internally by display() to show the problems _after_
+               the merge list where it is most likely to be seen, but if display()
+               is not going to be called then this method should be called explicitly
+               to ensure that the user is notified of problems with the graph.
+
+               All output goes to stderr, except for unsatisfied dependencies which
+               go to stdout for parsing by programs such as autounmask.
+               """
+
+               # Note that show_masked_packages() sends it's output to
+               # stdout, and some programs such as autounmask parse the
+               # output in cases when emerge bails out. However, when
+               # show_masked_packages() is called for installed packages
+               # here, the message is a warning that is more appropriate
+               # to send to stderr, so temporarily redirect stdout to
+               # stderr. TODO: Fix output code so there's a cleaner way
+               # to redirect everything to stderr.
+               sys.stdout.flush()
+               sys.stderr.flush()
+               stdout = sys.stdout
+               try:
+                       sys.stdout = sys.stderr
+                       self._display_problems()
+               finally:
+                       sys.stdout = stdout
+                       sys.stdout.flush()
+                       sys.stderr.flush()
+
+               # This goes to stdout for parsing by programs like autounmask.
+               for pargs, kwargs in self._dynamic_config._unsatisfied_deps_for_display:
+                       self._show_unsatisfied_dep(*pargs, **kwargs)
+
+       def _display_problems(self):
+               if self._dynamic_config._circular_deps_for_display is not None:
+                       self._show_circular_deps(
+                               self._dynamic_config._circular_deps_for_display)
+
+               # The user is only notified of a slot conflict if
+               # there are no unresolvable blocker conflicts.
+               if self._dynamic_config._unsatisfied_blockers_for_display is not None:
+                       self._show_unsatisfied_blockers(
+                               self._dynamic_config._unsatisfied_blockers_for_display)
+               elif self._dynamic_config._slot_collision_info:
+                       self._show_slot_collision_notice()
+               else:
+                       self._show_missed_update()
+
+               self._display_autounmask()
 
                # TODO: Add generic support for "set problem" handlers so that
                # the below warnings aren't special cases for world only.
index 46b29eca7e43192376a90e2dbe92693b42e67707..ddaa62602a9170412937203a9f5cccf6f78fb88d 100644 (file)
@@ -320,6 +320,13 @@ def help(myopts, havecolor=1):
                for line in wrap(desc, desc_width):
                        print(desc_indent + line)
                print()
+               print("       " + green("--autounmask-write") + " [ %s | %s ]" % \
+                       (turquoise("y"), turquoise("n")))
+               desc = "If --autounmask is enabled, changes are written " + \
+                       "to config files, respecting CONFIG_PROTECT."
+               for line in wrap(desc, desc_width):
+                       print(desc_indent + line)
+               print()
                print("       " + green("--backtrack") + " " + turquoise("COUNT"))
                desc = "Specifies an integer number of times to backtrack if " + \
                        "dependency calculation fails due to a conflict or an " + \
index ee0fc4ed069cd6755934754888fb54ae72953ca5..7921d7d1ce694b9ca9a119c2b33f55db103f29d1 100644 (file)
@@ -427,6 +427,7 @@ def insert_optional_args(args):
        default_arg_opts = {
                '--ask'                  : y_or_n,
                '--autounmask'           : y_or_n,
+               '--autounmask-write'     : y_or_n,
                '--buildpkg'             : y_or_n,
                '--complete-graph'       : y_or_n,
                '--deep'       : valid_integers,
@@ -598,6 +599,12 @@ def parse_opts(tmpcmdline, silent=False):
                        "choices" : true_y_or_n
                },
 
+               "--autounmask-write": {
+                       "help"    : "write changes made by --autounmask to disk",
+                       "type"    : "choice",
+                       "choices" : true_y_or_n
+               },
+
                "--accept-properties": {
                        "help":"temporarily override ACCEPT_PROPERTIES",
                        "action":"store"
@@ -916,6 +923,9 @@ def parse_opts(tmpcmdline, silent=False):
        if myoptions.autounmask in true_y:
                myoptions.autounmask = True
 
+       if myoptions.autounmask_write in true_y:
+               myoptions.autounmask_write = True
+
        if myoptions.buildpkg in true_y:
                myoptions.buildpkg = True
        else: