check EAPI of custom ebuilds
We want people to use EAPI=4 or newer, so start enforcing it in the
upload hook rather than trying to catch it at review time.
BUG=chromium:340036
TEST=`./pre-upload_unittest.py` passes
TEST=uploaded a few CLs with bad EAPIs & good EAPIs and checked behavior
Change-Id: I9132f21b8ee4cab1a7acdf7855a9505b7307649e
Reviewed-on: https://chromium-review.googlesource.com/184622
Reviewed-by: David James <davidjames@chromium.org>
Tested-by: Mike Frysinger <vapier@chromium.org>
diff --git a/pre-upload.py b/pre-upload.py
index 7837cb3..0cc52f1 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -222,6 +222,19 @@
return data
+def _get_file_content(path, commit):
+ """Returns the content of a file at a specific commit.
+
+ We can't rely on the file as it exists in the filesystem as people might be
+ uploading a series of changes which modifies the file multiple times.
+
+ Note: The "content" of a symlink is just the target. So if you're expecting
+ a full file, you should check that first. One way to detect is that the
+ content will not have any newlines.
+ """
+ return _run_command(['git', 'show', '%s:%s' % (commit, path)])
+
+
def _get_file_diff(path, commit):
"""Returns a list of (linenum, lines) tuples that the commit touched."""
output = _run_command(['git', 'show', '-p', '--pretty=format:',
@@ -241,12 +254,13 @@
return new_lines
-def _get_affected_files(commit, include_deletes=False):
+def _get_affected_files(commit, include_deletes=False, relative=False):
"""Returns list of absolute filepaths that were modified/added.
Args:
commit: The commit
include_deletes: If true we'll include delete in the list.
+ relative: Whether to return full paths to files.
Returns:
A list of modified/added (and perhaps deleted) files
@@ -257,8 +271,11 @@
m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
# Ignore deleted files, and return absolute paths of files
if include_deletes or m.group(1)[0] != 'D':
- pwd = os.getcwd()
- files.append(os.path.join(pwd, m.group(2)))
+ f = m.group(2)
+ if not relative:
+ pwd = os.getcwd()
+ f = os.path.join(pwd, f)
+ files.append(f)
return files
@@ -467,6 +484,70 @@
return None
+def _check_ebuild_eapi(project, commit):
+ """Make sure we have people use EAPI=4 or newer with custom ebuilds.
+
+ We want to get away from older EAPI's as it makes life confusing and they
+ have less builtin error checking.
+
+ Args:
+ project: The project to look at
+ commit: The commit to look at
+
+ Returns:
+ A HookFailure or None.
+ """
+ # If this is the portage-stable overlay, then ignore the check. It's rare
+ # that we're doing anything other than importing files from upstream, so
+ # forcing a rev bump makes no sense.
+ whitelist = (
+ 'chromiumos/overlays/portage-stable',
+ )
+ if project in whitelist:
+ return None
+
+ BAD_EAPIS = ('0', '1', '2', '3')
+
+ get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
+
+ ebuilds_re = [r'\.ebuild$']
+ ebuilds = _filter_files(_get_affected_files(commit, relative=True),
+ ebuilds_re)
+ bad_ebuilds = []
+
+ for ebuild in ebuilds:
+ # If the ebuild does not specify an EAPI, it defaults to 0.
+ eapi = '0'
+
+ lines = _get_file_content(ebuild, commit).splitlines()
+ if len(lines) == 1:
+ # This is most likely a symlink, so skip it entirely.
+ continue
+
+ for line in lines:
+ m = get_eapi.match(line)
+ if m:
+ # Once we hit the first EAPI line in this ebuild, stop processing.
+ # The spec requires that there only be one and it be first, so
+ # checking all possible values is pointless. We also assume that
+ # it's "the" EAPI line and not something in the middle of a heredoc.
+ eapi = m.group(1)
+ break
+
+ if eapi in BAD_EAPIS:
+ bad_ebuilds.append((ebuild, eapi))
+
+ if bad_ebuilds:
+ # pylint: disable=C0301
+ url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
+ # pylint: enable=C0301
+ return HookFailure(
+ 'These ebuilds are using old EAPIs. Please update to 4 or newer.\n'
+ '\t%s\n'
+ 'See this guide for more details:\n%s\n' %
+ ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
+
+
def _check_keywords(_project, commit):
"""Make sure we use the new style KEYWORDS when possible in ebuilds.
@@ -696,6 +777,7 @@
_check_change_has_valid_cq_depend,
_check_change_has_test_field,
_check_change_has_proper_changeid,
+ _check_ebuild_eapi,
_check_ebuild_licenses,
_check_no_stray_whitespace,
_check_no_long_lines,
diff --git a/pre-upload_unittest.py b/pre-upload_unittest.py
index 2aea653..57633e6 100755
--- a/pre-upload_unittest.py
+++ b/pre-upload_unittest.py
@@ -12,6 +12,8 @@
import os
import sys
+import errors
+
# pylint: disable=W0212
# We access private members of the pre_upload module all over the place.
@@ -106,5 +108,104 @@
self.assertFalse(failure)
+class CheckEbuildEapi(cros_test_lib.MockTestCase):
+ """Tests for _check_ebuild_eapi."""
+
+ PORTAGE_STABLE = 'chromiumos/overlays/portage-stable'
+
+ def setUp(self):
+ self.file_mock = self.PatchObject(pre_upload, '_get_affected_files')
+ self.content_mock = self.PatchObject(pre_upload, '_get_file_content')
+ self.diff_mock = self.PatchObject(pre_upload, '_get_file_diff',
+ side_effect=Exception())
+
+ def testSkipUpstreamOverlays(self):
+ """Skip ebuilds found in upstream overlays."""
+ self.file_mock.side_effect = Exception()
+ ret = pre_upload._check_ebuild_eapi(self.PORTAGE_STABLE, 'HEAD')
+ self.assertEqual(ret, None)
+
+ # Make sure our condition above triggers.
+ self.assertRaises(Exception, pre_upload._check_ebuild_eapi, 'o', 'HEAD')
+
+ def testSkipNonEbuilds(self):
+ """Skip non-ebuild files."""
+ self.content_mock.side_effect = Exception()
+
+ self.file_mock.return_value = ['some-file', 'ebuild/dir', 'an.ebuild~']
+ ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ self.assertEqual(ret, None)
+
+ # Make sure our condition above triggers.
+ self.file_mock.return_value.append('a/real.ebuild')
+ self.assertRaises(Exception, pre_upload._check_ebuild_eapi, 'o', 'HEAD')
+
+ def testSkipSymlink(self):
+ """Skip files that are just symlinks."""
+ self.file_mock.return_value = ['a-r1.ebuild']
+ self.content_mock.return_value = 'a.ebuild'
+ ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ self.assertEqual(ret, None)
+
+ def testRejectEapiImplicit0Content(self):
+ """Reject ebuilds that do not declare EAPI (so it's 0)."""
+ self.file_mock.return_value = ['a.ebuild']
+
+ self.content_mock.return_value = """# Header
+IUSE="foo"
+src_compile() { }
+"""
+ ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ self.assertTrue(isinstance (ret, errors.HookFailure))
+
+ def testRejectExplicitEapi1Content(self):
+ """Reject ebuilds that do declare old EAPI explicitly."""
+ self.file_mock.return_value = ['a.ebuild']
+
+ template = """# Header
+EAPI=%s
+IUSE="foo"
+src_compile() { }
+"""
+ # Make sure we only check the first EAPI= setting.
+ self.content_mock.return_value = template % '1\nEAPI=4'
+ ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ self.assertTrue(isinstance (ret, errors.HookFailure))
+
+ # Verify we handle double quotes too.
+ self.content_mock.return_value = template % '"1"'
+ ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ self.assertTrue(isinstance (ret, errors.HookFailure))
+
+ # Verify we handle single quotes too.
+ self.content_mock.return_value = template % "'1'"
+ ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ self.assertTrue(isinstance (ret, errors.HookFailure))
+
+ def testAcceptExplicitEapi4Content(self):
+ """Accept ebuilds that do declare new EAPI explicitly."""
+ self.file_mock.return_value = ['a.ebuild']
+
+ template = """# Header
+EAPI=%s
+IUSE="foo"
+src_compile() { }
+"""
+ # Make sure we only check the first EAPI= setting.
+ self.content_mock.return_value = template % '4\nEAPI=1'
+ ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ self.assertEqual(ret, None)
+
+ # Verify we handle double quotes too.
+ self.content_mock.return_value = template % '"5"'
+ ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ self.assertEqual(ret, None)
+
+ # Verify we handle single quotes too.
+ self.content_mock.return_value = template % "'5-hdepend'"
+ ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ self.assertEqual(ret, None)
+
+
if __name__ == '__main__':
cros_test_lib.main()