diff options
| -rwxr-xr-x | scripts/oe-go-mod-autogen.py | 663 |
1 files changed, 663 insertions, 0 deletions
diff --git a/scripts/oe-go-mod-autogen.py b/scripts/oe-go-mod-autogen.py new file mode 100755 index 00000000..09d6133b --- /dev/null +++ b/scripts/oe-go-mod-autogen.py | |||
| @@ -0,0 +1,663 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | |||
| 3 | import os | ||
| 4 | import sys | ||
| 5 | import logging | ||
| 6 | import argparse | ||
| 7 | from collections import OrderedDict | ||
| 8 | import subprocess | ||
| 9 | |||
| 10 | # This switch is used to make this script error out ASAP, mainly for debugging purpose | ||
| 11 | ERROR_OUT_ON_FETCH_AND_CHECKOUT_FAILURE = True | ||
| 12 | |||
| 13 | logger = logging.getLogger('oe-go-mod-autogen') | ||
| 14 | loggerhandler = logging.StreamHandler() | ||
| 15 | loggerhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) | ||
| 16 | logger.addHandler(loggerhandler) | ||
| 17 | logger.setLevel(logging.INFO) | ||
| 18 | |||
| 19 | class GoModTool(object): | ||
| 20 | def __init__(self, repo, rev, workdir): | ||
| 21 | self.repo = repo | ||
| 22 | self.rev = rev | ||
| 23 | self.workdir = workdir | ||
| 24 | |||
| 25 | # Stores the actual module name and its related information | ||
| 26 | # {module: (repo_url, repo_dest_dir, fullsrcrev)} | ||
| 27 | self.modules_repoinfo = {} | ||
| 28 | |||
| 29 | # {module_name: (url, version, destdir, fullsrcrev)} | ||
| 30 | # | ||
| 31 | # url: place to get the source codes, we only support git repo | ||
| 32 | # version: module version, git tag or git rev | ||
| 33 | # destdir: place to put the fetched source codes | ||
| 34 | # fullsrcrev: full src rev which is the value of SRC_REV | ||
| 35 | # | ||
| 36 | # e.g. | ||
| 37 | # For 'github.com/Masterminds/semver/v3 v3.1.1' in go.mod: | ||
| 38 | # module_name = github.com/Masterminds/semver/v3 | ||
| 39 | # url = https://github.com/Masterminds/semver | ||
| 40 | # version = v3.1.1 | ||
| 41 | # destdir = ${WORKDIR}/${BP}/src/${GO_IMPORT}/vendor/github.com/Masterminds/semver/v3 | ||
| 42 | # fullsrcrev = d387ce7889a157b19ad7694dba39a562051f41b0 | ||
| 43 | self.modules_require = OrderedDict() | ||
| 44 | |||
| 45 | # {orig_module: (actual_module, actual_version)} | ||
| 46 | self.modules_replace = OrderedDict() | ||
| 47 | |||
| 48 | # Unhandled modules | ||
| 49 | self.modules_unhandled = OrderedDict() | ||
| 50 | |||
| 51 | # store subpaths used to form srcpath | ||
| 52 | # {actual_module_name: subpath} | ||
| 53 | self.modules_subpaths = OrderedDict() | ||
| 54 | |||
| 55 | # modules's actual source paths, record those that are not the same with the module itself | ||
| 56 | self.modules_srcpaths = OrderedDict() | ||
| 57 | |||
| 58 | # store lines, comment removed | ||
| 59 | self.require_lines = [] | ||
| 60 | self.replace_lines = [] | ||
| 61 | |||
| 62 | # fetch repo | ||
| 63 | self.fetch_and_checkout_repo(self.repo.split('://')[1], self.repo, self.rev, checkout=True, get_subpath=False) | ||
| 64 | |||
| 65 | def show_go_mod_info(self): | ||
| 66 | # Print modules_require, modules_replace and modules_unhandled | ||
| 67 | print("modules required:") | ||
| 68 | for m in self.modules_require: | ||
| 69 | url, version, destdir, fullrev = self.modules_require[m] | ||
| 70 | print("%s %s %s %s" % (m, version, url, fullrev)) | ||
| 71 | |||
| 72 | print("modules replace:") | ||
| 73 | for m in self.modules_replace: | ||
| 74 | actual_module, actual_version = self.modules_replace[m] | ||
| 75 | print("%s => %s %s" % (m, actual_module, actual_version)) | ||
| 76 | |||
| 77 | print("modules unhandled:") | ||
| 78 | for m in self.modules_unhandled: | ||
| 79 | reason = self.modules_unhandled[m] | ||
| 80 | print("%s unhandled: %s" % (m, reason)) | ||
| 81 | |||
| 82 | def parse(self): | ||
| 83 | # check if this repo needs autogen | ||
| 84 | repo_url, repo_dest_dir, repo_fullrev = self.modules_repoinfo[self.repo.split('://')[1]] | ||
| 85 | if os.path.isdir(os.path.join(repo_dest_dir, 'vendor')): | ||
| 86 | logger.info("vendor direcotry has already existed for %s, no need to add other repos" % self.repo) | ||
| 87 | return | ||
| 88 | go_mod_file = os.path.join(repo_dest_dir, 'go.mod') | ||
| 89 | if not os.path.exists(go_mod_file): | ||
| 90 | logger.info("go.mod file does not exist for %s, no need to add otehr repos" % self.repo) | ||
| 91 | return | ||
| 92 | self.parse_go_mod(go_mod_file) | ||
| 93 | self.show_go_mod_info() | ||
| 94 | |||
| 95 | def fetch_and_checkout_repo(self, module_name, repo_url, rev, default_protocol='https://', checkout=False, get_subpath=True): | ||
| 96 | """ | ||
| 97 | Fetch repo_url to <workdir>/repos/repo_base_name | ||
| 98 | """ | ||
| 99 | protocol = default_protocol | ||
| 100 | if '://' in repo_url: | ||
| 101 | repo_url_final = repo_url | ||
| 102 | else: | ||
| 103 | repo_url_final = default_protocol + repo_url | ||
| 104 | logger.debug("fetch and checkout %s %s" % (repo_url_final, rev)) | ||
| 105 | repos_dir = os.path.join(self.workdir, 'repos') | ||
| 106 | if not os.path.exists(repos_dir): | ||
| 107 | os.makedirs(repos_dir) | ||
| 108 | repo_basename = repo_url.split('/')[-1].split('.git')[0] | ||
| 109 | repo_dest_dir = os.path.join(repos_dir, repo_basename) | ||
| 110 | module_last_name = module_name.split('/')[-1] | ||
| 111 | git_action = "fetch" | ||
| 112 | if os.path.exists(repo_dest_dir): | ||
| 113 | if checkout: | ||
| 114 | # check if current HEAD is rev | ||
| 115 | try: | ||
| 116 | headrev = subprocess.check_output('git rev-list -1 HEAD', shell=True, cwd=repo_dest_dir).decode('utf-8').strip() | ||
| 117 | requiredrev = subprocess.check_output('git rev-list -1 %s 2>/dev/null || git rev-list -1 %s/%s' % (rev, module_last_name, rev), shell=True, cwd=repo_dest_dir).decode('utf-8').strip() | ||
| 118 | if headrev == requiredrev: | ||
| 119 | logger.info("%s has already been fetched and checked out as required, skipping" % repo_url) | ||
| 120 | self.modules_repoinfo[module_name] = (repo_url, repo_dest_dir, requiredrev) | ||
| 121 | return | ||
| 122 | else: | ||
| 123 | logger.info("HEAD of %s is not %s, will do a clean clone" % (repo_dest_dir, requiredrev)) | ||
| 124 | git_action = "clone" | ||
| 125 | except: | ||
| 126 | logger.info("'git rev-list' in %s failed, will do a clean clone" % repo_dest_dir) | ||
| 127 | git_action = "clone" | ||
| 128 | else: | ||
| 129 | # determine if the current repo points to the desired remote repo | ||
| 130 | try: | ||
| 131 | remote_origin_url = subprocess.check_output('git config --get remote.origin.url', shell=True, cwd=repo_dest_dir).decode('utf-8').strip() | ||
| 132 | if remote_origin_url.endswith('.git'): | ||
| 133 | if not repo_url_final.endswith('.git'): | ||
| 134 | remote_origin_url = remote_origin_url[:-4] | ||
| 135 | else: | ||
| 136 | if repo_url_final.endswith('.git'): | ||
| 137 | remote_origin_url = remote_origin_url + '.git' | ||
| 138 | if remote_origin_url != repo_url_final: | ||
| 139 | logger.info("remote.origin.url for %s is not %s, will do a clean clone" % (repo_dest_dir, repo_url_final)) | ||
| 140 | git_action = "clone" | ||
| 141 | except: | ||
| 142 | logger.info("'git config --get remote.origin.url' in %s failed, will do a clean clone" % repo_dest_dir) | ||
| 143 | git_action = "clone" | ||
| 144 | else: | ||
| 145 | # No local repo, clone it. | ||
| 146 | git_action = "clone" | ||
| 147 | |||
| 148 | if git_action == "clone": | ||
| 149 | logger.info("Removing %s" % repo_dest_dir) | ||
| 150 | subprocess.check_call('rm -rf %s' % repo_dest_dir, shell=True) | ||
| 151 | |||
| 152 | # clone/fetch repo | ||
| 153 | try: | ||
| 154 | git_cwd = repos_dir if git_action == "clone" else repo_dest_dir | ||
| 155 | logger.info("git %s %s in %s" % (git_action, repo_url_final, git_cwd)) | ||
| 156 | subprocess.check_call('git %s %s >/dev/null 2>&1' % (git_action, repo_url_final), shell=True, cwd=git_cwd) | ||
| 157 | except: | ||
| 158 | logger.warning("Failed to %s %s in %s" % (git_action, repo_url_final, git_cwd)) | ||
| 159 | return | ||
| 160 | |||
| 161 | def get_requiredrev(get_subpath): | ||
| 162 | import re | ||
| 163 | # check if rev is a revision or a version | ||
| 164 | if len(rev) == 12 and re.match('[0-9a-f]+', rev): | ||
| 165 | rev_is_version = False | ||
| 166 | else: | ||
| 167 | rev_is_version = True | ||
| 168 | |||
| 169 | # if rev is not a version, 'git rev-list -1 <rev>' should just succeed! | ||
| 170 | if not rev_is_version: | ||
| 171 | try: | ||
| 172 | rev_return = subprocess.check_output('git rev-list -1 %s 2>/dev/null' % rev, shell=True, cwd=repo_dest_dir).decode('utf-8').strip() | ||
| 173 | if get_subpath: | ||
| 174 | cmd = 'git branch -M toremove && git checkout -b check_subpath %s && git branch -D toremove' % rev_return | ||
| 175 | subprocess.check_call(cmd, shell=True, cwd=repo_dest_dir) | ||
| 176 | # try to get the subpath for this module | ||
| 177 | module_name_parts = module_name.split('/') | ||
| 178 | while (len(module_name_parts) > 0): | ||
| 179 | subpath = '/'.join(module_name_parts) | ||
| 180 | dir_to_check = repo_dest_dir + '/' + '/'.join(module_name_parts) | ||
| 181 | if os.path.isdir(dir_to_check): | ||
| 182 | self.modules_subpaths[module_name] = subpath | ||
| 183 | break | ||
| 184 | else: | ||
| 185 | module_name_parts.pop(0) | ||
| 186 | return rev_return | ||
| 187 | except: | ||
| 188 | logger.warning("Revision (%s) not in repo(%s)" % (rev, repo_dest_dir)) | ||
| 189 | return None | ||
| 190 | |||
| 191 | # the following codes deals with case where rev is a version | ||
| 192 | # determine the longest match tag, in this way, we can get the current srcpath to be used in relocation.inc | ||
| 193 | # we first get the initial tag, which is formed from module_name and rev | ||
| 194 | module_parts = module_name.split('/') | ||
| 195 | if rev.startswith(module_parts[-1] + '.'): | ||
| 196 | tag = '/'.join(module_parts[:-1]) + '/' + rev | ||
| 197 | last_module_part_replaced = True | ||
| 198 | else: | ||
| 199 | tag = '/'.join(module_parts) + '/' + rev | ||
| 200 | last_module_part_replaced = False | ||
| 201 | logger.debug("use %s as the initial tag for %s" % (tag, module_name)) | ||
| 202 | tag_parts = tag.split('/') | ||
| 203 | while(len(tag_parts) > 0): | ||
| 204 | try: | ||
| 205 | rev_return = subprocess.check_output('git rev-list -1 %s 2>/dev/null' % tag, shell=True, cwd=repo_dest_dir).decode('utf-8').strip() | ||
| 206 | if len(tag_parts) > 1: | ||
| 207 | # ensure that the subpath exists | ||
| 208 | if get_subpath: | ||
| 209 | cmd = 'git branch -M toremove && git checkout -b check_subpath %s && git branch -D toremove' % rev_return | ||
| 210 | subprocess.check_call(cmd, shell=True, cwd=repo_dest_dir) | ||
| 211 | # get subpath for the actual_module_name | ||
| 212 | if last_module_part_replaced: | ||
| 213 | subpath = '/'.join(tag_parts[:-1]) + '/' + module_parts[-1] | ||
| 214 | if not os.path.isdir(repo_dest_dir + '/' + subpath): | ||
| 215 | subpath = '/'.join(tag_parts[:-1]) | ||
| 216 | else: | ||
| 217 | subpath = '/'.join(tag_parts[:-1]) | ||
| 218 | if not os.path.isdir(repo_dest_dir + '/' + subpath): | ||
| 219 | logger.warning("subpath (%s) derived from tag matching does not exist in %s" % (subpath, repo_dest_dir)) | ||
| 220 | return None | ||
| 221 | self.modules_subpaths[module_name] = subpath | ||
| 222 | logger.info("modules_subpath[%s] = %s" % (module_name, subpath)) | ||
| 223 | return rev_return | ||
| 224 | except: | ||
| 225 | tag_parts.pop(0) | ||
| 226 | tag = '/'.join(tag_parts) | ||
| 227 | logger.warning("No tag matching %s" % rev) | ||
| 228 | return None | ||
| 229 | |||
| 230 | requiredrev = get_requiredrev(get_subpath) | ||
| 231 | if requiredrev: | ||
| 232 | logger.info("Got module(%s) requiredrev: %s" % (module_name, requiredrev)) | ||
| 233 | if checkout: | ||
| 234 | subprocess.check_call('git checkout -b gomodautogen %s' % requiredrev, shell=True, cwd=repo_dest_dir) | ||
| 235 | self.modules_repoinfo[module_name] = (repo_url, repo_dest_dir, requiredrev) | ||
| 236 | else: | ||
| 237 | logger.warning("Failed to get requiredrev, repo_url = %s, rev = %s, module_name = %s" % (repo_url, rev, module_name)) | ||
| 238 | |||
| 239 | def parse_go_mod(self, go_mod_path): | ||
| 240 | """ | ||
| 241 | Parse go.mod file to get the modules info | ||
| 242 | """ | ||
| 243 | # First we get the require and replace lines | ||
| 244 | # The parsing logic assumes the replace lines come *after* the require lines | ||
| 245 | inrequire = False | ||
| 246 | inreplace = False | ||
| 247 | with open(go_mod_path, 'r') as f: | ||
| 248 | lines = f.readlines() | ||
| 249 | for line in lines: | ||
| 250 | if line.startswith('require ('): | ||
| 251 | inrequire = True | ||
| 252 | continue | ||
| 253 | if line.startswith(')'): | ||
| 254 | inrequire = False | ||
| 255 | continue | ||
| 256 | if line.startswith('require ') or inrequire: | ||
| 257 | # we have one line require | ||
| 258 | require_line = line.lstrip('require ').split('//')[0].strip() | ||
| 259 | if require_line: | ||
| 260 | self.require_lines.append(require_line) | ||
| 261 | continue | ||
| 262 | # we can deal with requires and replaces separately because go.mod always writes requires before replaces | ||
| 263 | if line.startswith('replace ('): | ||
| 264 | inreplace = True | ||
| 265 | continue | ||
| 266 | if line.startswith(')'): | ||
| 267 | inreplace = False | ||
| 268 | continue | ||
| 269 | if line.startswith('replace ') or inreplace: | ||
| 270 | replace_line = line.lstrip('replace ').split('//')[0].strip() | ||
| 271 | if replace_line: | ||
| 272 | self.replace_lines.append(replace_line) | ||
| 273 | continue | ||
| 274 | # | ||
| 275 | # parse the require_lines and replace_lines to form self.modules_require and self.modules_replace | ||
| 276 | # | ||
| 277 | logger.debug("Parsing require_lines and replace_lines ...") | ||
| 278 | # A typical replace line is as below: | ||
| 279 | # github.com/hashicorp/golang-lru => github.com/ktock/golang-lru v0.5.5-0.20211029085301-ec551be6f75c | ||
| 280 | # It means that the github.com/hashicorp/golang-lru module is replaced by github.com/ktock/golang-lru | ||
| 281 | # with the version 'v0.5.5-0.20211029085301-ec551be6f75c'. | ||
| 282 | # So the destdir is vendor/github.com/hashicorp/golang-lru while the contents are from github.com/ktock/golang-lru | ||
| 283 | for line in self.replace_lines: | ||
| 284 | orig_module, actual = line.split('=>') | ||
| 285 | actual_module, actual_version = actual.split() | ||
| 286 | orig_module = orig_module.strip() | ||
| 287 | actual_module = actual_module.strip() | ||
| 288 | actual_version = actual_version.strip() | ||
| 289 | self.modules_replace[orig_module] = (actual_module, actual_version) | ||
| 290 | # | ||
| 291 | # Typical require lines are as below: | ||
| 292 | # github.com/Masterminds/semver/v3 v3.1.1 | ||
| 293 | # golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 | ||
| 294 | # | ||
| 295 | # We need to first try https://<module_name>?=go-get=1 to see it contains | ||
| 296 | # line starting with '<meta name="go-import" content='. | ||
| 297 | # | ||
| 298 | # If so, get root-path vcs repo-url from content. See https://go.dev/ref/mod#vcs-find | ||
| 299 | # For example, the above 'wget https://golang.org/x/crypto?go-get=1' gives you | ||
| 300 | # <meta name="go-import" content="golang.org/x/crypto git https://go.googlesource.com/crypto"> | ||
| 301 | # In such case, the self.modules_require has the following contents: | ||
| 302 | # module_name: golang.org/x/crypto | ||
| 303 | # url: https://go.googlesource.com/crypto | ||
| 304 | # version: v0.0.0-20220321153916-2c7772ba3064 | ||
| 305 | # destdir: ${WORKDIR}/${BP}/src/import/vendor.fetch/golang.org/x/crypto | ||
| 306 | # fullsrcrev: 2c7772ba30643b7a2026cbea938420dce7c6384d (git rev-list -1 2c7772ba3064) | ||
| 307 | # | ||
| 308 | # If not, try https://pkg.go.dev/<module_name>, and find the 'Repository'. | ||
| 309 | # For example, 'wget https://pkg.go.dev/github.com/Masterminds/semver/v3' gives: | ||
| 310 | # github.com/Masterminds/semver | ||
| 311 | # In such case, the self.modules has the following contents: | ||
| 312 | # module_name: github.com/Masterminds/semver/v3 | ||
| 313 | # url: https://github.com/Masterminds/semver | ||
| 314 | # version: v3.1.1 | ||
| 315 | # destdir: ${WORKDIR}/${BP}/src/import/vendor.fetch/github.com/Masterminds/semver/v3 | ||
| 316 | # fullsrcrev: 7bb0c843b53d6ad21a3f619cb22c4b442bb3ef3e (git rev-list -1 v3.1.1) | ||
| 317 | # | ||
| 318 | # As a last resort, if the last component of <module_name> matches 'v[0-9]+', | ||
| 319 | # remove the last component and try wget https://<module_name_with_last_component_removed>?go-get=1, | ||
| 320 | # then try using the above matching method. | ||
| 321 | # | ||
| 322 | for line in self.require_lines: | ||
| 323 | module_name, version = line.strip().split() | ||
| 324 | logger.debug("require line: %s" % line) | ||
| 325 | logger.debug("module_name = %s; version = %s" % (module_name, version)) | ||
| 326 | # take the modules_replace into consideration to get the actual version and actual module name | ||
| 327 | # note that the module_name is used in destdir, and the actual_module_name and actual_version | ||
| 328 | # are used to determine the url and fullsrcrev | ||
| 329 | destdir = '${WORKDIR}/${BP}/src/import/vendor.fetch/%s' % module_name | ||
| 330 | actual_module_name = module_name | ||
| 331 | actual_version = version | ||
| 332 | if module_name in self.modules_replace: | ||
| 333 | actual_module_name, actual_version = self.modules_replace[module_name] | ||
| 334 | logger.debug("actual_module_name = %s; actual_version = %s" % (actual_module_name, actual_version)) | ||
| 335 | url, fullsrcrev = self.get_url_srcrev(actual_module_name, actual_version) | ||
| 336 | logger.debug("url = %s; fullsrcrev = %s" % (url, fullsrcrev)) | ||
| 337 | if url and fullsrcrev: | ||
| 338 | self.modules_require[module_name] = (url, version, destdir, fullsrcrev) | ||
| 339 | # form srcpath, actual_module_name/<subpath> | ||
| 340 | if actual_module_name in self.modules_subpaths: | ||
| 341 | subpath = self.modules_subpaths[actual_module_name] | ||
| 342 | srcpath = '%s/%s' % (actual_module_name, subpath) | ||
| 343 | self.modules_srcpaths[module_name] = srcpath | ||
| 344 | logger.info("self.modules_srcpaths[%s] = %s" % (module_name, srcpath)) | ||
| 345 | else: | ||
| 346 | self.modules_srcpaths[module_name] = actual_module_name | ||
| 347 | else: | ||
| 348 | logger.warning("get_url_srcrev(%s, %s) failed" % (actual_module_name, actual_version)) | ||
| 349 | if ERROR_OUT_ON_FETCH_AND_CHECKOUT_FAILURE: | ||
| 350 | sys.exit(1) | ||
| 351 | |||
| 352 | def use_wget_to_get_repo_url(self, wget_content_file, url_cache_file, module_name): | ||
| 353 | """ | ||
| 354 | Use wget to get repo_url for module_name, return None if not found | ||
| 355 | """ | ||
| 356 | try: | ||
| 357 | logger.info("wget -O %s https://%s?=go-get=1" % (wget_content_file, module_name)) | ||
| 358 | subprocess.check_call('wget -O %s https://%s?=go-get=1' % (wget_content_file, module_name), shell=True) | ||
| 359 | with open(wget_content_file, 'r') as f: | ||
| 360 | for line in f.readlines(): | ||
| 361 | if '<meta name="go-import" content=' in line: | ||
| 362 | logger.info("Succeed to find go-import content for %s" % module_name) | ||
| 363 | logger.debug("The line is %s" % line) | ||
| 364 | root_path, vcs, repo_url = line.split('content=')[1].split('"')[1].split() | ||
| 365 | logger.info("%s: %s %s %s" % (module_name, root_path, vcs, repo_url)) | ||
| 366 | if vcs != 'git': | ||
| 367 | logger.warning('%s unhandled as its vcs is %s which is not supported by this script.' % (module_name, vcs)) | ||
| 368 | unhandled_reason = 'vcs %s is not supported by this script' % vcs | ||
| 369 | self.modules_unhandled[module_name] = unhandled_reason | ||
| 370 | return None | ||
| 371 | with open(url_cache_file, 'w') as f: | ||
| 372 | f.write(repo_url) | ||
| 373 | return repo_url | ||
| 374 | except: | ||
| 375 | logger.info("wget -O %s https://%s?=go-get=1 failed" % (wget_content_file, module_name)) | ||
| 376 | # if we cannot find repo url from https://<module_name>?=go-get=1, try https://pkg.go/dev/<module_name> | ||
| 377 | try: | ||
| 378 | logger.info("wget -O %s https://pkg.go.dev/%s" % (wget_content_file, module_name)) | ||
| 379 | subprocess.check_call("wget -O %s https://pkg.go.dev/%s" % (wget_content_file, module_name), shell=True) | ||
| 380 | repo_url_found = False | ||
| 381 | with open(wget_content_file, 'r') as f: | ||
| 382 | in_repo_section = False | ||
| 383 | for line in f.readlines(): | ||
| 384 | if '>Repository<' in line: | ||
| 385 | in_repo_section = True | ||
| 386 | continue | ||
| 387 | if in_repo_section: | ||
| 388 | newline = line.strip() | ||
| 389 | if newline != '' and not newline.startswith('<'): | ||
| 390 | repo_url = newline | ||
| 391 | repo_url_found = True | ||
| 392 | break | ||
| 393 | if repo_url_found: | ||
| 394 | logger.info("repo url for %s: %s" % (module_name, repo_url)) | ||
| 395 | with open(url_cache_file, 'w') as f: | ||
| 396 | f.write(repo_url) | ||
| 397 | return repo_url | ||
| 398 | else: | ||
| 399 | unhandled_reason = 'cannot determine repo_url for %s' % module_name | ||
| 400 | self.modules_unhandled[module_name] = unhandled_reason | ||
| 401 | return None | ||
| 402 | except: | ||
| 403 | logger.info("wget -O %s https://pkg.go.dev/%s failed" % (wget_content_file, module_name)) | ||
| 404 | return None | ||
| 405 | |||
| 406 | |||
| 407 | def get_repo_url_rev(self, module_name, version): | ||
| 408 | """ | ||
| 409 | Return (repo_url, rev) | ||
| 410 | """ | ||
| 411 | import re | ||
| 412 | # First get rev from version | ||
| 413 | v = version.split('+incompatible')[0] | ||
| 414 | version_components = v.split('-') | ||
| 415 | if len(version_components) == 1: | ||
| 416 | rev = v | ||
| 417 | elif len(version_components) == 3: | ||
| 418 | if len(version_components[2]) == 12: | ||
| 419 | rev = version_components[2] | ||
| 420 | else: | ||
| 421 | rev = v | ||
| 422 | else: | ||
| 423 | rev = v | ||
| 424 | |||
| 425 | # | ||
| 426 | # Get repo_url | ||
| 427 | # We put a cache mechanism here, <wget_content_file>.repo_url.cache is used to store the repo url fetch before | ||
| 428 | # | ||
| 429 | wget_dir = os.path.join(self.workdir, 'wget-contents') | ||
| 430 | if not os.path.exists(wget_dir): | ||
| 431 | os.makedirs(wget_dir) | ||
| 432 | wget_content_file = os.path.join(wget_dir, module_name.replace('/', '_')) | ||
| 433 | url_cache_file = "%s.repo_url.cache" % wget_content_file | ||
| 434 | if os.path.exists(url_cache_file): | ||
| 435 | with open(url_cache_file, 'r') as f: | ||
| 436 | repo_url = f.readline().strip() | ||
| 437 | return (repo_url, rev) | ||
| 438 | module_name_parts = module_name.split('/') | ||
| 439 | while (len(module_name_parts) > 0): | ||
| 440 | module_name_to_check = '/'.join(module_name_parts) | ||
| 441 | logger.info("module_name_to_check: %s" % module_name_to_check) | ||
| 442 | repo_url = self.use_wget_to_get_repo_url(wget_content_file, url_cache_file, module_name_to_check) | ||
| 443 | if repo_url: | ||
| 444 | return (repo_url, rev) | ||
| 445 | else: | ||
| 446 | if module_name in self.modules_unhandled: | ||
| 447 | return (None, rev) | ||
| 448 | else: | ||
| 449 | module_name_parts.pop(-1) | ||
| 450 | |||
| 451 | unhandled_reason = 'cannot determine the repo for %s' % module_name | ||
| 452 | self.modules_unhandled[module_name] = unhandled_reason | ||
| 453 | return (None, rev) | ||
| 454 | |||
| 455 | def get_url_srcrev(self, module_name, version): | ||
| 456 | """ | ||
| 457 | Return url and fullsrcrev according to module_name and version | ||
| 458 | """ | ||
| 459 | repo_url, rev = self.get_repo_url_rev(module_name, version) | ||
| 460 | if not repo_url or not rev: | ||
| 461 | return (None, None) | ||
| 462 | self.fetch_and_checkout_repo(module_name, repo_url, rev) | ||
| 463 | if module_name in self.modules_repoinfo: | ||
| 464 | repo_url, repo_dest_dir, repo_fullrev = self.modules_repoinfo[module_name] | ||
| 465 | # remove the .git suffix to sync repos across modules with different versions and across recipes | ||
| 466 | if repo_url.endswith('.git'): | ||
| 467 | repo_url = repo_url[:-len('.git')] | ||
| 468 | return (repo_url, repo_fullrev) | ||
| 469 | else: | ||
| 470 | unhandled_reason = 'fetch_and_checkout_repo(%s, %s, %s) failed' % (module_name, repo_url, rev) | ||
| 471 | self.modules_unhandled[module_name] = unhandled_reason | ||
| 472 | return (None, None) | ||
| 473 | |||
| 474 | def gen_src_uri_inc(self): | ||
| 475 | """ | ||
| 476 | Generate src_uri.inc file containing SRC_URIs | ||
| 477 | """ | ||
| 478 | src_uri_inc_file = os.path.join(self.workdir, 'src_uri.inc') | ||
| 479 | # record the <name> after writting SRCREV_<name>, this is to avoid modules having the same basename resulting in same SRCREV_xxx | ||
| 480 | srcrev_name_recorded = [] | ||
| 481 | template = """# %s %s | ||
| 482 | # [1] git ls-remote %s %s | ||
| 483 | SRCREV_%s="%s" | ||
| 484 | SRC_URI += "git://%s;name=%s;protocol=https;nobranch=1;destsuffix=${WORKDIR}/${BP}/src/import/vendor.fetch/%s" | ||
| 485 | |||
| 486 | """ | ||
| 487 | # We can't simply write SRC_URIs one by one in the order that go.mod specify them. | ||
| 488 | # Because the latter one might clean things up for the former one if the former one is a subpath of the latter one. | ||
| 489 | def take_first_len(elem): | ||
| 490 | return len(elem[0]) | ||
| 491 | |||
| 492 | src_uri_contents = [] | ||
| 493 | with open(src_uri_inc_file, 'w') as f: | ||
| 494 | for module in self.modules_require: | ||
| 495 | # {module_name: (url, version, destdir, fullsrcrev)} | ||
| 496 | repo_url, version, destdir, fullrev = self.modules_require[module] | ||
| 497 | if module in self.modules_replace: | ||
| 498 | actual_module_name, actual_version = self.modules_replace[module] | ||
| 499 | else: | ||
| 500 | actual_module_name, actual_version = (module, version) | ||
| 501 | if '://' in repo_url: | ||
| 502 | repo_url_noprotocol = repo_url.split('://')[1] | ||
| 503 | else: | ||
| 504 | repo_url_noprotocol = repo_url | ||
| 505 | if not repo_url.startswith('https://'): | ||
| 506 | repo_url = 'https://' + repo_url | ||
| 507 | name = module.split('/')[-1] | ||
| 508 | if name in srcrev_name_recorded: | ||
| 509 | name = '-'.join(module.split('/')[-2:]) | ||
| 510 | src_uri_contents.append((actual_module_name, actual_version, repo_url, fullrev, name, fullrev, repo_url_noprotocol, name, actual_module_name)) | ||
| 511 | srcrev_name_recorded.append(name) | ||
| 512 | # sort the src_uri_contents and then write it | ||
| 513 | src_uri_contents.sort(key=take_first_len) | ||
| 514 | for content in src_uri_contents: | ||
| 515 | f.write(template % content) | ||
| 516 | logger.info("%s generated" % src_uri_inc_file) | ||
| 517 | |||
| 518 | def gen_relocation_inc(self): | ||
| 519 | """ | ||
| 520 | Generate relocation.inc file | ||
| 521 | """ | ||
| 522 | relocation_inc_file = os.path.join(self.workdir, 'relocation.inc') | ||
| 523 | template = """export sites="%s" | ||
| 524 | |||
| 525 | do_compile:prepend() { | ||
| 526 | cd ${S}/src/import | ||
| 527 | for s in $sites; do | ||
| 528 | site_dest=$(echo $s | cut -d: -f1) | ||
| 529 | site_source=$(echo $s | cut -d: -f2) | ||
| 530 | force_flag=$(echo $s | cut -d: -f3) | ||
| 531 | mkdir -p vendor.copy/$site_dest | ||
| 532 | if [ -n "$force_flag" ]; then | ||
| 533 | echo "[INFO] $site_dest: force copying .go files" | ||
| 534 | rm -rf vendor.copy/$site_dest | ||
| 535 | rsync -a --exclude='vendor/' --exclude='.git/' vendor.fetch/$site_source/ vendor.copy/$site_dest | ||
| 536 | else | ||
| 537 | [ -n "$(ls -A vendor.copy/$site_dest/*.go 2> /dev/null)" ] && { echo "[INFO] vendor.fetch/$site_source -> $site_dest: go copy skipped (files present)" ; true ; } || { echo "[INFO] $site_dest: copying .go files" ; rsync -a --exclude='vendor/' --exclude='.git/' vendor.fetch/$site_source/ vendor.copy/$site_dest ; } | ||
| 538 | fi | ||
| 539 | done | ||
| 540 | } | ||
| 541 | """ | ||
| 542 | sites = [] | ||
| 543 | for module in self.modules_require: | ||
| 544 | # <dest>:<source>[:force] | ||
| 545 | if module in self.modules_srcpaths: | ||
| 546 | srcpath = self.modules_srcpaths[module] | ||
| 547 | logger.debug("Using %s as srcpath of module (%s)" % (srcpath, module)) | ||
| 548 | else: | ||
| 549 | srcpath = module | ||
| 550 | sites.append("%s:%s:force" % (module, srcpath)) | ||
| 551 | # To avoid the former one being overriden by the latter one when the former one is a subpath of the latter one, sort sites | ||
| 552 | sites.sort(key=len) | ||
| 553 | with open(relocation_inc_file, 'w') as f: | ||
| 554 | sites_str = ' \\\n '.join(sites) | ||
| 555 | f.write(template % sites_str) | ||
| 556 | logger.info("%s generated" % relocation_inc_file) | ||
| 557 | |||
| 558 | def gen_modules_txt(self): | ||
| 559 | """ | ||
| 560 | Generate modules.txt file | ||
| 561 | """ | ||
| 562 | modules_txt_file = os.path.join(self.workdir, 'modules.txt') | ||
| 563 | with open(modules_txt_file, 'w') as f: | ||
| 564 | for l in self.require_lines: | ||
| 565 | f.write('# %s\n' % l) | ||
| 566 | f.write('## explicit\n') | ||
| 567 | for l in self.replace_lines: | ||
| 568 | f.write('# %s\n' %l) | ||
| 569 | logger.info("%s generated" % modules_txt_file) | ||
| 570 | |||
| 571 | def sanity_check(self): | ||
| 572 | """ | ||
| 573 | Various anity checks | ||
| 574 | """ | ||
| 575 | sanity_check_ok = True | ||
| 576 | # | ||
| 577 | # Sanity Check 1: | ||
| 578 | # For modules having the same repo, at most one is allowed to not have subpath. | ||
| 579 | # This check operates on self.modules_repoinfo and self.modules_subpaths | ||
| 580 | # | ||
| 581 | repo_modules = {} | ||
| 582 | for module in self.modules_repoinfo: | ||
| 583 | # first form {repo: [module1, module2, ...]} | ||
| 584 | repo_url, repo_dest_dir, fullsrcrev = self.modules_repoinfo[module] | ||
| 585 | if repo_url not in repo_modules: | ||
| 586 | repo_modules[repo_url] = [module] | ||
| 587 | else: | ||
| 588 | repo_modules[repo_url].append(module) | ||
| 589 | for repo in repo_modules: | ||
| 590 | modules = repo_modules[repo] | ||
| 591 | if len(modules) == 1: | ||
| 592 | continue | ||
| 593 | # for modules sharing the same repo, at most one is allowed to not have subpath | ||
| 594 | nosubpath_modules = [] | ||
| 595 | for m in modules: | ||
| 596 | if m not in self.modules_subpaths: | ||
| 597 | nosubpath_modules.append(m) | ||
| 598 | if len(nosubpath_modules) == 0: | ||
| 599 | continue | ||
| 600 | if len(nosubpath_modules) > 1: | ||
| 601 | logger.warning("Multiple modules sharing %s, but they don't have subpath: %s. Please double check." % (repo, nosubpath_modules)) | ||
| 602 | if len(nosubpath_modules) == 1: | ||
| 603 | # do further check, OK if the module is the prefix for other modules sharing the same repo | ||
| 604 | module_to_check = nosubpath_modules[0] | ||
| 605 | for m in modules: | ||
| 606 | if module_to_check == m: | ||
| 607 | continue | ||
| 608 | if not m.startswith('%s/' % module_to_check): | ||
| 609 | logger.warning("%s is sharing repo (%s) with other modules, and it might need a subpath. Please double check" % (module_to_check, repo)) | ||
| 610 | continue | ||
| 611 | |||
| 612 | # | ||
| 613 | # End of Sanity Check | ||
| 614 | # | ||
| 615 | if not sanity_check_ok: | ||
| 616 | sys.exit(1) | ||
| 617 | return | ||
| 618 | |||
| 619 | def main(): | ||
| 620 | parser = argparse.ArgumentParser( | ||
| 621 | description="oe-go-mod-autogen.py is used to generate src_uri.inc, relocation.inc and modules.txt to be used by go mod recipes", | ||
| 622 | epilog="Use %(prog)s --help to get help") | ||
| 623 | parser.add_argument("--repo", help = "Repo for the recipe.", required=True) | ||
| 624 | parser.add_argument("--rev", help = "Revision for the recipe.", required=True) | ||
| 625 | parser.add_argument("--module", help = "Go module name. To be used with '--test'") | ||
| 626 | parser.add_argument("--version", help = "Go module version. To be used with '--test'") | ||
| 627 | parser.add_argument("--test", help = "Test to get repo url and fullsrcrev, used together with --module and --version.", action="store_true") | ||
| 628 | parser.add_argument("--workdir", help = "Working directory to hold intermediate results and output.", default=os.getcwd()) | ||
| 629 | parser.add_argument("-d", "--debug", | ||
| 630 | help = "Enable debug output", | ||
| 631 | action="store_const", const=logging.DEBUG, dest="loglevel", default=logging.INFO) | ||
| 632 | parser.add_argument("-q", "--quiet", | ||
| 633 | help = "Hide all output except error messages", | ||
| 634 | action="store_const", const=logging.ERROR, dest="loglevel") | ||
| 635 | args = parser.parse_args() | ||
| 636 | |||
| 637 | logger.setLevel(args.loglevel) | ||
| 638 | logger.debug("oe-go-mod-autogen.py running for %s:%s in %s" % (args.repo, args.rev, args.workdir)) | ||
| 639 | gomodtool = GoModTool(args.repo, args.rev, args.workdir) | ||
| 640 | if args.test: | ||
| 641 | if not args.module or not args.version: | ||
| 642 | print("Please specify --module and --version") | ||
| 643 | sys.exit(1) | ||
| 644 | url, srcrev = gomodtool.get_url_srcrev(args.module, args.version) | ||
| 645 | print("url = %s, srcrev = %s" % (url, srcrev)) | ||
| 646 | if not url or not srcrev: | ||
| 647 | print("Failed to get url & srcrev for %s:%s" % (args.module, args.version)) | ||
| 648 | else: | ||
| 649 | gomodtool.parse() | ||
| 650 | gomodtool.sanity_check() | ||
| 651 | gomodtool.gen_src_uri_inc() | ||
| 652 | gomodtool.gen_relocation_inc() | ||
| 653 | gomodtool.gen_modules_txt() | ||
| 654 | |||
| 655 | |||
| 656 | if __name__ == "__main__": | ||
| 657 | try: | ||
| 658 | ret = main() | ||
| 659 | except Exception as esc: | ||
| 660 | ret = 1 | ||
| 661 | import traceback | ||
| 662 | traceback.print_exc() | ||
| 663 | sys.exit(ret) | ||
