Add repohook to check ebuild license/copyright
We will use the GPLv2 license and Google LLC copyright for new ebuilds
created by the COS team. This repohook enforces that new ebuild files in
cos/overlays/board-overlays contain GPLv2 license and Google LLC
copyright headers. Existing ebuilds with the ChromeOS license/copyright
are left alone.
BUG=b/149121748
TEST=./pre-upload_unittest.py and manual test in
cos/overlays/board-overlays
Change-Id: I76694ce86d825296c7e38ed4239368d3c5986490
diff --git a/pre-upload.py b/pre-upload.py
index b0c3f30..569df9d 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -1536,6 +1536,75 @@
return errors
+def _check_cos_ebuild_license_header(_project, commit, options=()):
+ """Verifies the license/copyright header for COS ebuild sources.
+
+ New ebuild files originated from COS team should have the GPLv2 license
+ and "Google LLC" copyright header.
+ """
+ LICENSE_HEADER = (
+ r"""^#
+# Copyright (20[0-9]{2}) Google LLC
+#
+# This program is free software; you can redistribute it and\/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation\.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE\. See the
+# GNU General Public License for more details\.
+#$
+"""
+ )
+ license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
+
+ included, excluded = _parse_common_inclusion_options(options)
+
+ bad_files = []
+ bad_year_files = []
+
+ files = _filter_files(
+ _get_affected_files(commit, relative=True),
+ included + [r'.*\.ebuild'],
+ excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
+ existing_files = set(_get_affected_files(commit, relative=True,
+ include_adds=False))
+
+ current_year = str(datetime.datetime.now().year)
+ for f in files:
+ # We only want to check for new ebuild files
+ if f in existing_files:
+ continue
+
+ contents = _get_file_content(f, commit)
+ if not contents:
+ # Ignore empty files.
+ continue
+
+ m = license_re.search(contents)
+ if not m:
+ bad_files.append(f)
+
+ if m and f not in existing_files:
+ year = m.group(1)
+ if year != current_year:
+ bad_year_files.append(f)
+
+ errors = []
+ if bad_files:
+ msg = '%s:\n%s\n%s' % (
+ 'License must match', license_re.pattern,
+ 'Include Google copyright and GPLv2 license in new files:')
+ errors.append(HookFailure(msg, bad_files))
+ if bad_year_files:
+ msg = 'Use current year (%s) in copyright headers in new files:' % (
+ current_year)
+ errors.append(HookFailure(msg, bad_year_files))
+
+ return errors
+
+
def _check_aosp_license(_project, commit, options=()):
"""Verifies the AOSP license/copyright header.
@@ -2072,6 +2141,7 @@
'chromiumos/third_party/kernel-next': [_kernel_configcheck],
'cos/manifest': [_check_cos_license],
'cos/repohooks': [_check_cos_license],
+ 'cos/overlays/board-overlays': [_check_cos_ebuild_license_header],
}
diff --git a/pre-upload_unittest.py b/pre-upload_unittest.py
index 489f4ba..759f63c 100755
--- a/pre-upload_unittest.py
+++ b/pre-upload_unittest.py
@@ -1141,6 +1141,178 @@
self.assertFalse(pre_upload._check_cos_license('proj', 'sha1'))
+class CheckCOSEbuildLicenseCopyrightHeader(PreUploadTestCase):
+ """Tests for _check_cos_ebuild_license_header."""
+
+ def setUp(self):
+ self.file_mock = self.PatchObject(pre_upload, '_get_affected_files')
+ self.content_mock = self.PatchObject(pre_upload, '_get_file_content')
+
+ def testHeaders(self):
+ """Accept old header styles."""
+ header = u"""#
+# Copyright 2020 Google LLC
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+"""
+ def fake_get_affected_files(_, relative, include_adds=True):
+ _ = relative
+ if include_adds:
+ return ['test.ebuild']
+ else:
+ return []
+
+ self.file_mock.side_effect = fake_get_affected_files
+ self.content_mock.return_value = header
+ self.assertFalse(
+ pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+ def testRejectNoLinesAround(self):
+ """Reject headers missing the empty lines before/after the license."""
+ header = u"""# Copyright 2020 Google LLC
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+"""
+ def fake_get_affected_files(_, relative, include_adds=True):
+ _ = relative
+ if include_adds:
+ return ['test.ebuild']
+ else:
+ return []
+
+ self.file_mock.side_effect = fake_get_affected_files
+ self.content_mock.return_value = header
+ self.assertTrue(
+ pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+ def testNewFileYear(self):
+ """Added files should have the current year in license header."""
+ year = datetime.datetime.now().year
+ HEADERS = (
+ u"""#
+# Copyright 2015 Google LLC
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+# Copyright 2015 Google LLC
+#
+""",
+ u"""#
+# Copyright {} Google LLC
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+""".format(year),
+ )
+ want_error = (True, False)
+ def fake_get_affected_files(_, relative, include_adds=True):
+ _ = relative
+ if include_adds:
+ return ['test.ebuild']
+ else:
+ return []
+
+ self.file_mock.side_effect = fake_get_affected_files
+ for i, header in enumerate(HEADERS):
+ self.content_mock.return_value = header
+ if want_error[i]:
+ self.assertTrue(
+ pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+ else:
+ self.assertFalse(
+ pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+ def testIgnoreExistingEbuilds(self):
+ """Existing ebuild files with ChromiumOS license/copyright are accepted."""
+ header = (
+ u'# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.\n'
+ u'# Use of this source code is governed by a BSD-style license that'
+ u' can be\n'
+ u'# found in the LICENSE file.\n'
+ )
+ self.file_mock.return_value = ['test.ebuild']
+ self.content_mock.return_value = header
+ self.assertFalse(
+ pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+ def testIgnoreNonEbuildFiles(self):
+ """Added non-ebuild files with ChromiumOS license/copyright are ignored."""
+ header = (
+ u'# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.\n'
+ u'# Use of this source code is governed by a BSD-style license that'
+ u' can be\n'
+ u'# found in the LICENSE file.\n'
+ )
+
+ def fake_get_affected_files(_, relative, include_adds=True):
+ _ = relative
+ if include_adds:
+ return ['file.py']
+ else:
+ return []
+
+ self.file_mock.side_effect = fake_get_affected_files
+ self.content_mock.return_value = header
+ self.assertFalse(
+ pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+ def testIgnoreExcludedPaths(self):
+ """Ignores excluded paths for license checks."""
+ def fake_get_affected_files(_, relative, include_adds=True):
+ _ = relative
+ if include_adds:
+ return ['foo/OWNERS']
+ else:
+ return []
+
+ self.file_mock.side_effect = fake_get_affected_files
+ self.content_mock.return_value = u'owner@chromium.org'
+ self.assertFalse(
+ pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+ def testIgnoreTopLevelExcludedPaths(self):
+ """Ignores excluded paths for license checks."""
+ def fake_get_affected_files(_, relative, include_adds=True):
+ _ = relative
+ if include_adds:
+ return ['OWNERS']
+ else:
+ return []
+
+ self.file_mock.side_effect = fake_get_affected_files
+ self.content_mock.return_value = u'owner@chromium.org'
+ self.assertFalse(
+ pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+
class CheckLayoutConfTestCase(PreUploadTestCase):
"""Tests for _check_layout_conf."""