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 | ||