diff options
| -rwxr-xr-x | repo | 89 | ||||
| -rw-r--r-- | requirements.json | 57 | ||||
| -rw-r--r-- | tests/test_wrapper.py | 76 |
3 files changed, 222 insertions, 0 deletions
| @@ -246,6 +246,7 @@ GITC_FS_ROOT_DIR = '/gitc/manifest-rw/' | |||
| 246 | 246 | ||
| 247 | import collections | 247 | import collections |
| 248 | import errno | 248 | import errno |
| 249 | import json | ||
| 249 | import optparse | 250 | import optparse |
| 250 | import re | 251 | import re |
| 251 | import shutil | 252 | import shutil |
| @@ -1035,6 +1036,90 @@ def _ParseArguments(args): | |||
| 1035 | return cmd, opt, arg | 1036 | return cmd, opt, arg |
| 1036 | 1037 | ||
| 1037 | 1038 | ||
| 1039 | class Requirements(object): | ||
| 1040 | """Helper for checking repo's system requirements.""" | ||
| 1041 | |||
| 1042 | REQUIREMENTS_NAME = 'requirements.json' | ||
| 1043 | |||
| 1044 | def __init__(self, requirements): | ||
| 1045 | """Initialize. | ||
| 1046 | |||
| 1047 | Args: | ||
| 1048 | requirements: A dictionary of settings. | ||
| 1049 | """ | ||
| 1050 | self.requirements = requirements | ||
| 1051 | |||
| 1052 | @classmethod | ||
| 1053 | def from_dir(cls, path): | ||
| 1054 | return cls.from_file(os.path.join(path, cls.REQUIREMENTS_NAME)) | ||
| 1055 | |||
| 1056 | @classmethod | ||
| 1057 | def from_file(cls, path): | ||
| 1058 | try: | ||
| 1059 | with open(path, 'rb') as f: | ||
| 1060 | data = f.read() | ||
| 1061 | except EnvironmentError: | ||
| 1062 | # NB: EnvironmentError is used for Python 2 & 3 compatibility. | ||
| 1063 | # If we couldn't open the file, assume it's an old source tree. | ||
| 1064 | return None | ||
| 1065 | |||
| 1066 | return cls.from_data(data) | ||
| 1067 | |||
| 1068 | @classmethod | ||
| 1069 | def from_data(cls, data): | ||
| 1070 | comment_line = re.compile(br'^ *#') | ||
| 1071 | strip_data = b''.join(x for x in data.splitlines() if not comment_line.match(x)) | ||
| 1072 | try: | ||
| 1073 | json_data = json.loads(strip_data) | ||
| 1074 | except Exception: # pylint: disable=broad-except | ||
| 1075 | # If we couldn't parse it, assume it's incompatible. | ||
| 1076 | return None | ||
| 1077 | |||
| 1078 | return cls(json_data) | ||
| 1079 | |||
| 1080 | def _get_soft_ver(self, pkg): | ||
| 1081 | """Return the soft version for |pkg| if it exists.""" | ||
| 1082 | return self.requirements.get(pkg, {}).get('soft', ()) | ||
| 1083 | |||
| 1084 | def _get_hard_ver(self, pkg): | ||
| 1085 | """Return the hard version for |pkg| if it exists.""" | ||
| 1086 | return self.requirements.get(pkg, {}).get('hard', ()) | ||
| 1087 | |||
| 1088 | @staticmethod | ||
| 1089 | def _format_ver(ver): | ||
| 1090 | """Return a dotted version from |ver|.""" | ||
| 1091 | return '.'.join(str(x) for x in ver) | ||
| 1092 | |||
| 1093 | def assert_ver(self, pkg, curr_ver): | ||
| 1094 | """Verify |pkg|'s |curr_ver| is new enough.""" | ||
| 1095 | curr_ver = tuple(curr_ver) | ||
| 1096 | soft_ver = tuple(self._get_soft_ver(pkg)) | ||
| 1097 | hard_ver = tuple(self._get_hard_ver(pkg)) | ||
| 1098 | if curr_ver < hard_ver: | ||
| 1099 | print('repo: error: Your version of "%s" (%s) is unsupported; ' | ||
| 1100 | 'Please upgrade to at least version %s to continue.' % | ||
| 1101 | (pkg, self._format_ver(curr_ver), self._format_ver(soft_ver)), | ||
| 1102 | file=sys.stderr) | ||
| 1103 | sys.exit(1) | ||
| 1104 | |||
| 1105 | if curr_ver < soft_ver: | ||
| 1106 | print('repo: warning: Your version of "%s" (%s) is no longer supported; ' | ||
| 1107 | 'Please upgrade to at least version %s to avoid breakage.' % | ||
| 1108 | (pkg, self._format_ver(curr_ver), self._format_ver(soft_ver)), | ||
| 1109 | file=sys.stderr) | ||
| 1110 | |||
| 1111 | def assert_all(self): | ||
| 1112 | """Assert all of the requirements are satisified.""" | ||
| 1113 | # See if we need a repo launcher upgrade first. | ||
| 1114 | self.assert_ver('repo', VERSION) | ||
| 1115 | |||
| 1116 | # Check python before we try to import the repo code. | ||
| 1117 | self.assert_ver('python', sys.version_info) | ||
| 1118 | |||
| 1119 | # Check git while we're at it. | ||
| 1120 | self.assert_ver('git', ParseGitVersion()) | ||
| 1121 | |||
| 1122 | |||
| 1038 | def _Usage(): | 1123 | def _Usage(): |
| 1039 | gitc_usage = "" | 1124 | gitc_usage = "" |
| 1040 | if get_gitc_manifest_dir(): | 1125 | if get_gitc_manifest_dir(): |
| @@ -1192,6 +1277,10 @@ def main(orig_args): | |||
| 1192 | print("fatal: unable to find repo entry point", file=sys.stderr) | 1277 | print("fatal: unable to find repo entry point", file=sys.stderr) |
| 1193 | sys.exit(1) | 1278 | sys.exit(1) |
| 1194 | 1279 | ||
| 1280 | reqs = Requirements.from_dir(os.path.dirname(repo_main)) | ||
| 1281 | if reqs: | ||
| 1282 | reqs.assert_all() | ||
| 1283 | |||
| 1195 | ver_str = '.'.join(map(str, VERSION)) | 1284 | ver_str = '.'.join(map(str, VERSION)) |
| 1196 | me = [sys.executable, repo_main, | 1285 | me = [sys.executable, repo_main, |
| 1197 | '--repo-dir=%s' % rel_repo_dir, | 1286 | '--repo-dir=%s' % rel_repo_dir, |
diff --git a/requirements.json b/requirements.json new file mode 100644 index 00000000..86b9a46c --- /dev/null +++ b/requirements.json | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | # This file declares various requirements for this version of repo. The | ||
| 2 | # launcher script will load it and check the constraints before trying to run | ||
| 3 | # us. This avoids issues of the launcher using an old version of Python (e.g. | ||
| 4 | # 3.5) while the codebase has moved on to requiring something much newer (e.g. | ||
| 5 | # 3.8). If the launcher tried to import us, it would fail with syntax errors. | ||
| 6 | |||
| 7 | # This is a JSON file with line-level comments allowed. | ||
| 8 | |||
| 9 | # Always keep backwards compatibility in mine. The launcher script is robust | ||
| 10 | # against missing values, but when a field is renamed/removed, it means older | ||
| 11 | # versions of the launcher script won't be able to enforce the constraint. | ||
| 12 | |||
| 13 | # When requiring versions, always use lists as they are easy to parse & compare | ||
| 14 | # in Python. Strings would require futher processing to turn into a list. | ||
| 15 | |||
| 16 | # Version constraints should be expressed in pairs: soft & hard. Soft versions | ||
| 17 | # are when we start warning users that their software too old and we're planning | ||
| 18 | # on dropping support for it, so they need to start planning system upgrades. | ||
| 19 | # Hard versions are when we refuse to work the tool. Users will be shown an | ||
| 20 | # error message before we abort entirely. | ||
| 21 | |||
| 22 | # When deciding whether to upgrade a version requirement, check out the distro | ||
| 23 | # lists to see who will be impacted: | ||
| 24 | # https://gerrit.googlesource.com/git-repo/+/HEAD/docs/release-process.md#Project-References | ||
| 25 | |||
| 26 | { | ||
| 27 | # The repo launcher itself. This allows us to force people to upgrade as some | ||
| 28 | # ignore the warnings about it being out of date, or install ancient versions | ||
| 29 | # to start with for whatever reason. | ||
| 30 | # | ||
| 31 | # NB: Repo launchers started checking this file with repo-2.12, so listing | ||
| 32 | # versions older than that won't make a difference. | ||
| 33 | "repo": { | ||
| 34 | "hard": [2, 11], | ||
| 35 | "soft": [2, 11] | ||
| 36 | }, | ||
| 37 | |||
| 38 | # Supported Python versions. | ||
| 39 | # | ||
| 40 | # python-3.6 is in Ubuntu Bionic. | ||
| 41 | # python-3.5 is in Debian Stretch. | ||
| 42 | "python": { | ||
| 43 | "hard": [3, 5], | ||
| 44 | "soft": [3, 6] | ||
| 45 | }, | ||
| 46 | |||
| 47 | # Supported git versions. | ||
| 48 | # | ||
| 49 | # git-1.7.2 is in Debian Squeeze. | ||
| 50 | # git-1.7.9 is in Ubuntu Precise. | ||
| 51 | # git-1.9.1 is in Ubuntu Trusty. | ||
| 52 | # git-1.7.10 is in Debian Wheezy. | ||
| 53 | "git": { | ||
| 54 | "hard": [1, 7, 2], | ||
| 55 | "soft": [1, 9, 1] | ||
| 56 | } | ||
| 57 | } | ||
diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index d8713738..6400faf4 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py | |||
| @@ -19,6 +19,7 @@ from io import StringIO | |||
| 19 | import os | 19 | import os |
| 20 | import re | 20 | import re |
| 21 | import shutil | 21 | import shutil |
| 22 | import sys | ||
| 22 | import tempfile | 23 | import tempfile |
| 23 | import unittest | 24 | import unittest |
| 24 | from unittest import mock | 25 | from unittest import mock |
| @@ -255,6 +256,81 @@ class CheckGitVersion(RepoWrapperTestCase): | |||
| 255 | self.wrapper._CheckGitVersion() | 256 | self.wrapper._CheckGitVersion() |
| 256 | 257 | ||
| 257 | 258 | ||
| 259 | class Requirements(RepoWrapperTestCase): | ||
| 260 | """Check Requirements handling.""" | ||
| 261 | |||
| 262 | def test_missing_file(self): | ||
| 263 | """Don't crash if the file is missing (old version).""" | ||
| 264 | testdir = os.path.dirname(os.path.realpath(__file__)) | ||
| 265 | self.assertIsNone(self.wrapper.Requirements.from_dir(testdir)) | ||
| 266 | self.assertIsNone(self.wrapper.Requirements.from_file( | ||
| 267 | os.path.join(testdir, 'xxxxxxxxxxxxxxxxxxxxxxxx'))) | ||
| 268 | |||
| 269 | def test_corrupt_data(self): | ||
| 270 | """If the file can't be parsed, don't blow up.""" | ||
| 271 | self.assertIsNone(self.wrapper.Requirements.from_file(__file__)) | ||
| 272 | self.assertIsNone(self.wrapper.Requirements.from_data(b'x')) | ||
| 273 | |||
| 274 | def test_valid_data(self): | ||
| 275 | """Make sure we can parse the file we ship.""" | ||
| 276 | self.assertIsNotNone(self.wrapper.Requirements.from_data(b'{}')) | ||
| 277 | rootdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) | ||
| 278 | self.assertIsNotNone(self.wrapper.Requirements.from_dir(rootdir)) | ||
| 279 | self.assertIsNotNone(self.wrapper.Requirements.from_file(os.path.join( | ||
| 280 | rootdir, 'requirements.json'))) | ||
| 281 | |||
| 282 | def test_format_ver(self): | ||
| 283 | """Check format_ver can format.""" | ||
| 284 | self.assertEqual('1.2.3', self.wrapper.Requirements._format_ver((1, 2, 3))) | ||
| 285 | self.assertEqual('1', self.wrapper.Requirements._format_ver([1])) | ||
| 286 | |||
| 287 | def test_assert_all_unknown(self): | ||
| 288 | """Check assert_all works with incompatible file.""" | ||
| 289 | reqs = self.wrapper.Requirements({}) | ||
| 290 | reqs.assert_all() | ||
| 291 | |||
| 292 | def test_assert_all_new_repo(self): | ||
| 293 | """Check assert_all accepts new enough repo.""" | ||
| 294 | reqs = self.wrapper.Requirements({'repo': {'hard': [1, 0]}}) | ||
| 295 | reqs.assert_all() | ||
| 296 | |||
| 297 | def test_assert_all_old_repo(self): | ||
| 298 | """Check assert_all rejects old repo.""" | ||
| 299 | reqs = self.wrapper.Requirements({'repo': {'hard': [99999, 0]}}) | ||
| 300 | with self.assertRaises(SystemExit): | ||
| 301 | reqs.assert_all() | ||
| 302 | |||
| 303 | def test_assert_all_new_python(self): | ||
| 304 | """Check assert_all accepts new enough python.""" | ||
| 305 | reqs = self.wrapper.Requirements({'python': {'hard': sys.version_info}}) | ||
| 306 | reqs.assert_all() | ||
| 307 | |||
| 308 | def test_assert_all_old_repo(self): | ||
| 309 | """Check assert_all rejects old repo.""" | ||
| 310 | reqs = self.wrapper.Requirements({'python': {'hard': [99999, 0]}}) | ||
| 311 | with self.assertRaises(SystemExit): | ||
| 312 | reqs.assert_all() | ||
| 313 | |||
| 314 | def test_assert_ver_unknown(self): | ||
| 315 | """Check assert_ver works with incompatible file.""" | ||
| 316 | reqs = self.wrapper.Requirements({}) | ||
| 317 | reqs.assert_ver('xxx', (1, 0)) | ||
| 318 | |||
| 319 | def test_assert_ver_new(self): | ||
| 320 | """Check assert_ver allows new enough versions.""" | ||
| 321 | reqs = self.wrapper.Requirements({'git': {'hard': [1, 0], 'soft': [2, 0]}}) | ||
| 322 | reqs.assert_ver('git', (1, 0)) | ||
| 323 | reqs.assert_ver('git', (1, 5)) | ||
| 324 | reqs.assert_ver('git', (2, 0)) | ||
| 325 | reqs.assert_ver('git', (2, 5)) | ||
| 326 | |||
| 327 | def test_assert_ver_old(self): | ||
| 328 | """Check assert_ver rejects old versions.""" | ||
| 329 | reqs = self.wrapper.Requirements({'git': {'hard': [1, 0], 'soft': [2, 0]}}) | ||
| 330 | with self.assertRaises(SystemExit): | ||
| 331 | reqs.assert_ver('git', (0, 5)) | ||
| 332 | |||
| 333 | |||
| 258 | class NeedSetupGnuPG(RepoWrapperTestCase): | 334 | class NeedSetupGnuPG(RepoWrapperTestCase): |
| 259 | """Check NeedSetupGnuPG behavior.""" | 335 | """Check NeedSetupGnuPG behavior.""" |
| 260 | 336 | ||
