diff options
author | Chen Qi <Qi.Chen@windriver.com> | 2023-09-04 20:22:00 -0700 |
---|---|---|
committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2023-09-15 17:30:40 +0000 |
commit | 23ea9c77f9c3c55c94cf44117d0e07ec154eff0c (patch) | |
tree | 683f72c448fa748e475ee5385606ed7db1dfcc1d /scripts | |
parent | 86ec0fea15e1f7f10328a7fb7cd46711e76185f7 (diff) | |
download | meta-virtualization-23ea9c77f9c3c55c94cf44117d0e07ec154eff0c.tar.gz |
oe-go-mod-autogen.py: add script to help adding/upgrading go mod recipes
oe-go-mod-autogen.py is a helper script for go mod recipes. It follows
Bruce's initiative about how to deal with go mod recipes in OE.
Example:
cmd: <path_to>/meta-virtualization/scripts/oe-go-mod-autogen.py \
--repo https://github.com/docker/compose --rev v2.20.3
output: src_uri.inc, relocation.inc, modules.txt
Copy these three generated files to replace the original ones,
then we only need update PV and SRCREV, and docker-compose is upgraded.
Below are some technical details.
* get module's repo from module name
This script checks the following two URLs to determine the module's repo.
1. https://<module_name_tweaked>?=go-get=1
2. https://pkg.go.dev/<module_name_tweaked>
The module_name_tweaked is derived from module_name, with the last components
removed one by one. Let me use two examples to explain this.
For module_name sigs.k8s.io/json, the sigs.k8s.io/json is first used as
module_name_tweaked for searching. And we can correctly get the repo URL, so
the search stops.
For module_name github.com/k3s-io/etcd/api/v3, the following ones are used
as module_name_tweaked:
github.com/k3s-io/etcd/api/v3
github.com/k3s-io/etcd/api
github.com/k3s-io/etcd
And when searching 'github.com/k3s-io/etcd', we get the repo URL, so the search
stops.
* determine the srcdir:destdir mapping in 'vendor' creation
To correctly form the 'vendor' directory, the mapping is critical.
This script makes use of tag matching and path matching to determine
the subpath in the repo for the module.
* avoid subpath being overriden by parent path
We need to avoid subpath being overriden by parent path. This is needed
for both SRC_URI ordering in src_uri.inc and the sites mapping ordering
in relocation.inc. This script simply uses the length as the ordering key,
simply for the reason that if a path is a subpath of another path, it must
be longer.
* the .git suffix is removed to sync with each other
Unlike normal recipes, go mod recipe usually have many SRC_URIs. This script
remove the '.git' suffix from repo URL so that the repo URLs are in sync
with each.
* basic directory hierarchy and caching mechanism
<cwd>/repos: hold the repos downloaded and checked
<cwd>/wget-contents: hold the contents to determine the module's repo
<cwd>/wget-contents/<module_name>.repo_url.cache: the repo value cache
This is to avoid unnecessary URL fetching and repo cloning.
* the ERROR_OUT_ON_FETCH_AND_CHECKOUT_FAILURE switch in script
The script must get the correct repo_url, fullsrc_rev and subpath for
each required module in go.mod to correctly generate the src_uri.inc and
relocation.inc files. If this process fails for any required module, this
script stop immediately, as I deliberately set ERROR_OUT_ON_FETCH_AND_CHECKOUT_FAILURE
to True in this script. The purpose is to encourage people to report
problems to meta-virt so that we can improve this script according to
these feedbacks. But this variable can set to False, then the script
only records the failed modules in self.modules_unhandled with reasons
added, people can modify the generated src_uri.inc and relocation.inc
to manually handle these unhandled modules if they are urgent to
add/upgrade some go mod recipes.
Signed-off-by: Chen Qi <Qi.Chen@windriver.com>
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'scripts')
-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) | ||