diff options
Diffstat (limited to 'scripts/oe-go-mod-autogen.py')
-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) | ||