summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChen Qi <Qi.Chen@windriver.com>2023-09-04 20:22:00 -0700
committerBruce Ashfield <bruce.ashfield@gmail.com>2023-09-15 17:30:40 +0000
commit23ea9c77f9c3c55c94cf44117d0e07ec154eff0c (patch)
tree683f72c448fa748e475ee5385606ed7db1dfcc1d
parent86ec0fea15e1f7f10328a7fb7cd46711e76185f7 (diff)
downloadmeta-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>
-rwxr-xr-xscripts/oe-go-mod-autogen.py663
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
3import os
4import sys
5import logging
6import argparse
7from collections import OrderedDict
8import subprocess
9
10# This switch is used to make this script error out ASAP, mainly for debugging purpose
11ERROR_OUT_ON_FETCH_AND_CHECKOUT_FAILURE = True
12
13logger = logging.getLogger('oe-go-mod-autogen')
14loggerhandler = logging.StreamHandler()
15loggerhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
16logger.addHandler(loggerhandler)
17logger.setLevel(logging.INFO)
18
19class 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
483SRCREV_%s="%s"
484SRC_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
525do_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
619def 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
656if __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)