Update gmerge to support deep upgrades.

With --deep mode, gmerge updates all necessary packages. This is useful for
ensuring dependencies are up to date.

Other features added in this CL:
 - /usr/local/portage is rm -rf'd at beginning of gmerge as well in case
   leftover state is there from a failed merge.
 - Improve error message when devserver is not running.
 - The --extra flag allows you to specify extra flags to emerge. This is useful
   for example to exclude certain packages from the upgrade. E.g
    gmerge -Dn chromeos -x '--usepkg-exclude=trousers'

BUG=chromium-os:15606, chromium-os:11332, chromium-os:14748
TEST=Try gmerging a deep upgrade of power manager. If packages that use enewuser
     / enewgroup are excluded, we can upgrade the rest of the packages. If the
     latest toolchain is used which includes getent, we can upgrade those
     packages.

Change-Id: Id01b35133486ec9448b62c80194d61ff1360e87e
Reviewed-on: http://gerrit.chromium.org/gerrit/1320
Reviewed-by: Chris Sosa <sosa@chromium.org>
Tested-by: David James <davidjames@chromium.org>
diff --git a/builder.py b/builder.py
index 2f93141..df93d20 100644
--- a/builder.py
+++ b/builder.py
@@ -86,7 +86,7 @@
   xpak.tbz2(out_path).recompose_mem(x)
 
 
-def _UpdateGmergeBinhost(board, pkg):
+def _UpdateGmergeBinhost(board, pkg, deep):
   """Add pkg to our gmerge-specific binhost.
 
   Files matching DEFAULT_INSTALL_MASK are not included in the tarball.
@@ -110,10 +110,16 @@
                                          settings=bintree.settings)
   gmerge_tree.populate()
 
-  # Create lists of matching packages.
-  gmerge_matches = set(gmerge_tree.dbapi.match(pkg))
-  bindb_matches = set(bintree.dbapi.match(pkg))
-  installed_matches = set(vardb.match(pkg)) & bindb_matches
+  if deep:
+    # If we're in deep mode, fill in the binhost completely.
+    gmerge_matches = set(gmerge_tree.dbapi.cpv_all())
+    bindb_matches = set(bintree.dbapi.cpv_all())
+    installed_matches = set(vardb.cpv_all()) & bindb_matches
+  else:
+    # Otherwise, just fill in the requested package.
+    gmerge_matches = set(gmerge_tree.dbapi.match(pkg))
+    bindb_matches = set(bintree.dbapi.match(pkg))
+    installed_matches = set(vardb.match(pkg)) & bindb_matches
 
   # Remove any stale packages that exist in the local binhost but are not
   # installed anymore.
@@ -142,6 +148,7 @@
       if old_build_time == build_time:
         continue
 
+    cherrypy.log('Filtering install mask from %s' % pkg, 'BUILD')
     _FilterInstallMaskFromPackage(build_path, gmerge_path)
     changed = True
 
@@ -206,7 +213,8 @@
           return self.SetError('Could not emerge ' + pkg)
 
       # Sync gmerge binhost.
-      if not _UpdateGmergeBinhost(board, pkg):
+      deep = additional_args.get('deep')
+      if not _UpdateGmergeBinhost(board, pkg, deep):
         return self.SetError('Package %s is not installed' % pkg)
 
       return 'Success\n'
diff --git a/gmerge b/gmerge
index 16ef174..0a23ef5 100755
--- a/gmerge
+++ b/gmerge
@@ -75,6 +75,7 @@
   def GeneratePackageRequest(self, package_name):
     """Build the POST string that conveys our options to the devserver."""
     post_data = {'board': self.board_name,
+                 'deep': FLAGS.deep or '',
                  'pkg': package_name,
                  'features': os.environ.get('FEATURES'),
                  'use': os.environ.get('USE'),
@@ -96,6 +97,8 @@
     except urllib2.HTTPError, e:
       # The exception includes the content, which is the error mesage
       sys.exit(e.read())
+    except urllib2.URLError, e:
+      sys.exit('Could not reach devserver. Reason: %s' % e.reason)
 
 
 def main():
@@ -111,13 +114,25 @@
                                          '(e.g. debug symbols)'))
   parser.add_option('-n', '--usepkg',
                     action='store_true', dest='usepkg', default=False,
-                    help='Use binary packages.')
+                    help='Use currently built binary packages on server.')
+  parser.add_option('-D', '--deep',
+                    action='store_true', dest='deep', default=False,
+                    help='Update package and all dependencies '
+                         '(requires --usepkg).')
+  parser.add_option('-x', '--extra', dest='extra', default='',
+                    help='Extra arguments to pass to emerge command.')
 
   (FLAGS, remaining_arguments) = parser.parse_args()
   if len(remaining_arguments) != 1:
     parser.print_help()
     sys.exit('Need exactly one package name')
 
+  # TODO(davidjames): Should we allow --deep without --usepkg? Not sure what
+  # the desired behavior should be in this case, so disabling the combo for
+  # now.
+  if FLAGS.deep and not FLAGS.usepkg:
+    sys.exit('If using --deep, --usepkg must also be enabled.')
+
   package_name = remaining_arguments[0]
 
   try:
@@ -128,10 +143,14 @@
     print 'Emerging ', package_name
     merger.SetupPortageEnvironment(os.environ)
     merger.RemountOrChangeRoot(os.environ)
-    subprocess.check_call([
-        'emerge', '--getbinpkgonly', '--usepkgonly', package_name])
-    subprocess.check_call([
-        'rm', '-rf', '/usr/local/portage'])
+    subprocess.check_call(['rm', '-rf', '/usr/local/portage'])
+    emerge_args = 'emerge --getbinpkgonly --usepkgonly --verbose'
+    if FLAGS.deep:
+      emerge_args += ' --update --deep'
+    if FLAGS.extra:
+      emerge_args += ' ' + FLAGS.extra
+    emerge_args += ' ' + package_name
+    subprocess.check_call(emerge_args, shell=True)
   finally:
     subprocess.call(['mount', '-o', 'remount,noexec', '/tmp'])