summaryrefslogtreecommitdiffstats
path: root/repo
diff options
context:
space:
mode:
authorMike Frysinger <vapier@google.com>2021-01-07 22:14:25 -0500
committerMike Frysinger <vapier@google.com>2021-01-19 16:48:21 +0000
commite5670c881225ed025c77e0362a7c7edcc912ef9f (patch)
tree39bff697aee8c2b318e7e10c7a5e92f365b6f439 /repo
parent48b2d10d8f7565173ca53bed0d0be15323512de4 (diff)
downloadgit-repo-e5670c881225ed025c77e0362a7c7edcc912ef9f.tar.gz
launcher: add a requirements framework to declare version dependencies
Currently we don't have a way for the checked out repo version to declare the version of tools it needs before we start running it. For somethings, like git, it's not a big deal as it can handle all the asserts itself. But for things like Python, it's impossible to reliably check before executing. We're in this state now: - we've been allowing Python 3.4, so the launcher accepts it - the repo codebase starts using Python 3.6 features - launcher tries to import us but hits syntax errors - user is left confused and assuming new repo is broken because they're seeing syntax errors This scenario is playing out with old launchers that still accept Python 2, and will continue to play out as time goes on and we want to require newer versions of Python 3. Lets create a JSON file to declare all these system requirements. That file format is extremely stable, so loading & parsing from even ancient versions of Python shouldn't be a problem. Then the launcher can read these settings and check the system state before attempting to execute any code. If the tools are too old, it can clearly diagnose & display information to the user as to the real problem (and not emit tracebacks or syntax errors). We have a couple of different tool version checks already (git, python, ssh) and can harmonize them in a single place. This also allows us to assert a reverse dependency if the need ever comes up: force the user to upgrade their `repo` launcher before we'll let them run us. Even though the launcher warns whenever a newer release is available, some users seem to ignore that, or they don't use repo that often (on the scale of years), and their upgrade jump is so dramatic that they fall back into the syntax error pit. Hopefully by the end of the year we can assume enough people have upgraded their launcher such that we can delete all of the duplicate version checks in the codebase. But until then, we'll keep them to maintain coverage. Change-Id: I5c12bbffdfd0a8ce978f39aa7f4674026fe9f4f8 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/293003 Reviewed-by: Michael Mortensen <mmortensen@google.com> Tested-by: Mike Frysinger <vapier@google.com>
Diffstat (limited to 'repo')
-rwxr-xr-xrepo89
1 files changed, 89 insertions, 0 deletions
diff --git a/repo b/repo
index 8f13015f..8936f57b 100755
--- a/repo
+++ b/repo
@@ -246,6 +246,7 @@ GITC_FS_ROOT_DIR = '/gitc/manifest-rw/'
246 246
247import collections 247import collections
248import errno 248import errno
249import json
249import optparse 250import optparse
250import re 251import re
251import shutil 252import shutil
@@ -1035,6 +1036,90 @@ def _ParseArguments(args):
1035 return cmd, opt, arg 1036 return cmd, opt, arg
1036 1037
1037 1038
1039class 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
1038def _Usage(): 1123def _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,