pre-upload: Check for invalid USE-setting in make.*.

Check that $USE isn't overwritten or referenced before it's
been defined in Portage make.conf and make.defaults files.

Also update pre-upload*.py to pass existing checks.

BUG=chromium:453966
TEST=added unit tests; also manually tested with broken and
     non-broken make.conf and make.defaults files

Change-Id: I77a6b52ba966c72ae6933f30616c0396dd856912
Reviewed-on: https://chromium-review.googlesource.com/251131
Commit-Queue: Dan Erat <derat@chromium.org>
Trybot-Ready: Dan Erat <derat@chromium.org>
Tested-by: Dan Erat <derat@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/errors.py b/errors.py
index 1a0f7dc..95f5e72 100644
--- a/errors.py
+++ b/errors.py
@@ -20,6 +20,9 @@
     self.msg = msg
     self.items = items
 
+  def __str__(self):
+    return _FormatHookFailure(self)
+
 
 _INDENT = ' ' * 4
 _PROJECT_INFO = 'Errors in PROJECT *%s*!'
diff --git a/pre-upload.py b/pre-upload.py
index 5279da5..d858eaa 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/python2
 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
@@ -721,6 +721,46 @@
                       for x in bad_ebuilds]), url))
 
 
+def _check_portage_make_use_var(_project, commit):
+  """Verify that $USE is set correctly in make.conf and make.defaults."""
+  files = _filter_files(_get_affected_files(commit, relative=True),
+                        [r'(^|/)make.(conf|defaults)$'])
+
+  errors = []
+  for path in files:
+    basename = os.path.basename(path)
+
+    # Has a USE= line already been encountered in this file?
+    saw_use = False
+
+    for i, line in enumerate(_get_file_content(path, commit).splitlines(), 1):
+      if not line.startswith('USE='):
+        continue
+
+      preserves_use = '${USE}' in line or '$USE' in line
+
+      if (basename == 'make.conf' or
+          (basename == 'make.defaults' and saw_use)) and not preserves_use:
+        errors.append('%s:%d: missing ${USE}' % (path, i))
+      elif basename == 'make.defaults' and not saw_use and preserves_use:
+        errors.append('%s:%d: ${USE} referenced in initial declaration' %
+                      (path, i))
+
+      saw_use = True
+
+  if errors:
+    return HookFailure(
+        'One or more Portage make files appear to set USE incorrectly.\n'
+        '\n'
+        'All USE assignments in make.conf and all assignments after the\n'
+        'initial declaration in make.defaults should contain "${USE}" to\n'
+        'preserve previously-set flags.\n'
+        '\n'
+        'The initial USE declaration in make.defaults should not contain\n'
+        '"${USE}".\n',
+        errors)
+
+
 def _check_change_has_proper_changeid(_project, commit):
   """Verify that Change-ID is present in last paragraph of commit message."""
   CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
@@ -1070,12 +1110,13 @@
     _check_ebuild_keywords,
     _check_ebuild_licenses,
     _check_ebuild_virtual_pv,
-    _check_no_stray_whitespace,
-    _check_no_long_lines,
+    _check_for_uprev,
     _check_layout_conf,
     _check_license,
+    _check_no_long_lines,
+    _check_no_stray_whitespace,
     _check_no_tabs,
-    _check_for_uprev,
+    _check_portage_make_use_var,
 ]
 
 
diff --git a/pre-upload_unittest.py b/pre-upload_unittest.py
index 9bdcfa9..c0b4ea4 100755
--- a/pre-upload_unittest.py
+++ b/pre-upload_unittest.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/python2
 # -*- coding: utf-8 -*-
 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
@@ -21,7 +21,6 @@
                                 '..', '..'))
 
 from chromite.cbuildbot import constants
-from chromite.lib import cros_build_lib
 from chromite.lib import cros_test_lib
 from chromite.lib import git
 from chromite.lib import osutils
@@ -39,7 +38,9 @@
   def runTest(self):
     self.assertEquals(u'', pre_upload._try_utf8_decode(''))
     self.assertEquals(u'abc', pre_upload._try_utf8_decode('abc'))
-    self.assertEquals(u'你好布萊恩', pre_upload._try_utf8_decode('你好布萊恩'))
+    self.assertEquals(
+        u'你好布萊恩',
+        pre_upload._try_utf8_decode('你好布萊恩'))
     # Invalid UTF-8
     self.assertEquals('\x80', pre_upload._try_utf8_decode('\x80'))
 
@@ -156,6 +157,54 @@
     self.assertFalse(failure)
 
 
+class CheckPortageMakeUseVar(cros_test_lib.MockTestCase):
+  """Tests for _check_portage_make_use_var."""
+
+  def setUp(self):
+    self.file_mock = self.PatchObject(pre_upload, '_get_affected_files')
+    self.content_mock = self.PatchObject(pre_upload, '_get_file_content')
+
+  def testMakeConfOmitsOriginalUseValue(self):
+    """Fail for make.conf that discards the previous value of $USE."""
+    self.file_mock.return_value = ['make.conf']
+    self.content_mock.return_value = 'USE="foo"\nUSE="${USE} bar"'
+    failure = pre_upload._check_portage_make_use_var('PROJECT', 'COMMIT')
+    self.assertTrue(failure, failure)
+
+  def testMakeConfCorrectUsage(self):
+    """Succeed for make.conf that preserves the previous value of $USE."""
+    self.file_mock.return_value = ['make.conf']
+    self.content_mock.return_value = 'USE="${USE} foo"\nUSE="${USE}" bar'
+    failure = pre_upload._check_portage_make_use_var('PROJECT', 'COMMIT')
+    self.assertFalse(failure, failure)
+
+  def testMakeDefaultsReferencesOriginalUseValue(self):
+    """Fail for make.defaults that refers to a not-yet-set $USE value."""
+    self.file_mock.return_value = ['make.defaults']
+    self.content_mock.return_value = 'USE="${USE} foo"'
+    failure = pre_upload._check_portage_make_use_var('PROJECT', 'COMMIT')
+    self.assertTrue(failure, failure)
+
+    # Also check for "$USE" without curly brackets.
+    self.content_mock.return_value = 'USE="$USE foo"'
+    failure = pre_upload._check_portage_make_use_var('PROJECT', 'COMMIT')
+    self.assertTrue(failure, failure)
+
+  def testMakeDefaultsOverwritesUseValue(self):
+    """Fail for make.defaults that discards its own $USE value."""
+    self.file_mock.return_value = ['make.defaults']
+    self.content_mock.return_value = 'USE="foo"\nUSE="bar"'
+    failure = pre_upload._check_portage_make_use_var('PROJECT', 'COMMIT')
+    self.assertTrue(failure, failure)
+
+  def testMakeDefaultsCorrectUsage(self):
+    """Succeed for make.defaults that sets and preserves $USE."""
+    self.file_mock.return_value = ['make.defaults']
+    self.content_mock.return_value = 'USE="foo"\nUSE="${USE}" bar'
+    failure = pre_upload._check_portage_make_use_var('PROJECT', 'COMMIT')
+    self.assertFalse(failure, failure)
+
+
 class CheckEbuildEapi(cros_test_lib.MockTestCase):
   """Tests for _check_ebuild_eapi."""