aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/oe-go-mod-autogen.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/oe-go-mod-autogen.py')
-rwxr-xr-xscripts/oe-go-mod-autogen.py800
1 files changed, 800 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..c44c096a
--- /dev/null
+++ b/scripts/oe-go-mod-autogen.py
@@ -0,0 +1,800 @@
+#!/usr/bin/env python3
+
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# go-dep processor
+#
+# Copyright (C) 2022 Bruce Ashfield
+# Copyright (C) 2023 Chen Qi
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import os
+import sys
+import logging
+import argparse
+from collections import OrderedDict
+import subprocess
+import textwrap
+
+# This switch is used to make this script error out ASAP, mainly for debugging purpose
+ERROR_OUT_ON_FETCH_AND_CHECKOUT_FAILURE = True
+
+logger = logging.getLogger('oe-go-mod-autogen')
+loggerhandler = logging.StreamHandler()
+loggerhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
+logger.addHandler(loggerhandler)
+logger.setLevel(logging.INFO)
+
+class GoModTool(object):
+ def __init__(self, repo, rev, workdir):
+ self.repo = repo
+ self.rev = rev
+ self.workdir = workdir
+
+ # Stores the actual module name and its related information
+ # {module: (repo_url, repo_dest_dir, fullsrcrev)}
+ self.modules_repoinfo = {}
+
+ # {module_name: (url, version, destdir, fullsrcrev)}
+ #
+ # url: place to get the source codes, we only support git repo
+ # version: module version, git tag or git rev
+ # destdir: place to put the fetched source codes
+ # fullsrcrev: full src rev which is the value of SRC_REV
+ #
+ # e.g.
+ # For 'github.com/Masterminds/semver/v3 v3.1.1' in go.mod:
+ # module_name = github.com/Masterminds/semver/v3
+ # url = https://github.com/Masterminds/semver
+ # version = v3.1.1
+ # destdir = ${WORKDIR}/${BP}/src/${GO_IMPORT}/vendor/github.com/Masterminds/semver/v3
+ # fullsrcrev = d387ce7889a157b19ad7694dba39a562051f41b0
+ self.modules_require = OrderedDict()
+
+ # {orig_module: (actual_module, actual_version)}
+ self.modules_replace = OrderedDict()
+
+ # Unhandled modules
+ self.modules_unhandled = OrderedDict()
+
+ # store subpaths used to form srcpath
+ # {actual_module_name: subpath}
+ self.modules_subpaths = OrderedDict()
+
+ # modules's actual source paths, record those that are not the same with the module itself
+ self.modules_srcpaths = OrderedDict()
+
+ # store lines, comment removed
+ self.require_lines = []
+ self.replace_lines = []
+
+ # fetch repo
+ self.fetch_and_checkout_repo(self.repo.split('://')[1], self.repo, self.rev, checkout=True, get_subpath=False)
+
+ def show_go_mod_info(self):
+ # Print modules_require, modules_replace and modules_unhandled
+ print("modules required:")
+ for m in self.modules_require:
+ url, version, destdir, fullrev = self.modules_require[m]
+ print("%s %s %s %s" % (m, version, url, fullrev))
+
+ print("modules replace:")
+ for m in self.modules_replace:
+ actual_module, actual_version = self.modules_replace[m]
+ print("%s => %s %s" % (m, actual_module, actual_version))
+
+ print("modules unhandled:")
+ for m in self.modules_unhandled:
+ reason = self.modules_unhandled[m]
+ print("%s unhandled: %s" % (m, reason))
+
+ def parse(self):
+ # check if this repo needs autogen
+ repo_url, repo_dest_dir, repo_fullrev = self.modules_repoinfo[self.repo.split('://')[1]]
+ if os.path.isdir(os.path.join(repo_dest_dir, 'vendor')):
+ logger.info("vendor direcotry has already existed for %s, no need to add other repos" % self.repo)
+ return
+ go_mod_file = os.path.join(repo_dest_dir, 'go.mod')
+ if not os.path.exists(go_mod_file):
+ logger.info("go.mod file does not exist for %s, no need to add otehr repos" % self.repo)
+ return
+ self.parse_go_mod(go_mod_file)
+ self.show_go_mod_info()
+
+ def fetch_and_checkout_repo(self, module_name, repo_url, rev, default_protocol='https://', checkout=False, get_subpath=True):
+ """
+ Fetch repo_url to <workdir>/repos/repo_base_name
+ """
+ protocol = default_protocol
+ if '://' in repo_url:
+ repo_url_final = repo_url
+ else:
+ repo_url_final = default_protocol + repo_url
+ logger.debug("fetch and checkout %s %s" % (repo_url_final, rev))
+ repos_dir = os.path.join(self.workdir, 'repos')
+ if not os.path.exists(repos_dir):
+ os.makedirs(repos_dir)
+ repo_basename = repo_url.split('/')[-1].split('.git')[0]
+ repo_dest_dir = os.path.join(repos_dir, repo_basename)
+ module_last_name = module_name.split('/')[-1]
+ git_action = "fetch"
+ if os.path.exists(repo_dest_dir):
+ if checkout:
+ # check if current HEAD is rev
+ try:
+ headrev = subprocess.check_output('git rev-list -1 HEAD', shell=True, cwd=repo_dest_dir).decode('utf-8').strip()
+ 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()
+ if headrev == requiredrev:
+ logger.info("%s has already been fetched and checked out as required, skipping" % repo_url)
+ self.modules_repoinfo[module_name] = (repo_url, repo_dest_dir, requiredrev)
+ return
+ else:
+ logger.info("HEAD of %s is not %s, will do a clean clone" % (repo_dest_dir, requiredrev))
+ git_action = "clone"
+ except:
+ logger.info("'git rev-list' in %s failed, will do a clean clone" % repo_dest_dir)
+ git_action = "clone"
+ else:
+ # determine if the current repo points to the desired remote repo
+ try:
+ remote_origin_url = subprocess.check_output('git config --get remote.origin.url', shell=True, cwd=repo_dest_dir).decode('utf-8').strip()
+ if remote_origin_url.endswith('.git'):
+ if not repo_url_final.endswith('.git'):
+ remote_origin_url = remote_origin_url[:-4]
+ else:
+ if repo_url_final.endswith('.git'):
+ remote_origin_url = remote_origin_url + '.git'
+ if remote_origin_url != repo_url_final:
+ logger.info("remote.origin.url for %s is not %s, will do a clean clone" % (repo_dest_dir, repo_url_final))
+ git_action = "clone"
+ except:
+ logger.info("'git config --get remote.origin.url' in %s failed, will do a clean clone" % repo_dest_dir)
+ git_action = "clone"
+ else:
+ # No local repo, clone it.
+ git_action = "clone"
+
+ if git_action == "clone":
+ logger.info("Removing %s" % repo_dest_dir)
+ subprocess.check_call('rm -rf %s' % repo_dest_dir, shell=True)
+
+ # clone/fetch repo
+ try:
+ git_cwd = repos_dir if git_action == "clone" else repo_dest_dir
+ logger.info("git %s %s in %s" % (git_action, repo_url_final, git_cwd))
+ subprocess.check_call('git %s %s >/dev/null 2>&1' % (git_action, repo_url_final), shell=True, cwd=git_cwd)
+ except:
+ logger.warning("Failed to %s %s in %s" % (git_action, repo_url_final, git_cwd))
+ return
+
+ def get_requiredrev(get_subpath):
+ import re
+ # check if rev is a revision or a version
+ if len(rev) == 12 and re.match('[0-9a-f]+', rev):
+ rev_is_version = False
+ else:
+ rev_is_version = True
+
+ # if rev is not a version, 'git rev-list -1 <rev>' should just succeed!
+ if not rev_is_version:
+ try:
+ rev_return = subprocess.check_output('git rev-list -1 %s 2>/dev/null' % rev, shell=True, cwd=repo_dest_dir).decode('utf-8').strip()
+ if get_subpath:
+ cmd = 'git branch -M toremove && git checkout -b check_subpath %s && git branch -D toremove' % rev_return
+ subprocess.check_call(cmd, shell=True, cwd=repo_dest_dir)
+ # try to get the subpath for this module
+ module_name_parts = module_name.split('/')
+ while (len(module_name_parts) > 0):
+ subpath = '/'.join(module_name_parts)
+ dir_to_check = repo_dest_dir + '/' + '/'.join(module_name_parts)
+ if os.path.isdir(dir_to_check):
+ self.modules_subpaths[module_name] = subpath
+ break
+ else:
+ module_name_parts.pop(0)
+ return rev_return
+ except:
+ logger.warning("Revision (%s) not in repo(%s)" % (rev, repo_dest_dir))
+ return None
+
+ # the following codes deals with case where rev is a version
+ # determine the longest match tag, in this way, we can get the current srcpath to be used in relocation.inc
+ # we first get the initial tag, which is formed from module_name and rev
+ module_parts = module_name.split('/')
+ if rev.startswith(module_parts[-1] + '.'):
+ tag = '/'.join(module_parts[:-1]) + '/' + rev
+ last_module_part_replaced = True
+ else:
+ tag = '/'.join(module_parts) + '/' + rev
+ last_module_part_replaced = False
+ logger.debug("use %s as the initial tag for %s" % (tag, module_name))
+ tag_parts = tag.split('/')
+ while(len(tag_parts) > 0):
+ try:
+ rev_return = subprocess.check_output('git rev-list -1 %s 2>/dev/null' % tag, shell=True, cwd=repo_dest_dir).decode('utf-8').strip()
+ if len(tag_parts) > 1:
+ # ensure that the subpath exists
+ if get_subpath:
+ cmd = 'git branch -M toremove && git checkout -b check_subpath %s && git branch -D toremove' % rev_return
+ subprocess.check_call(cmd, shell=True, cwd=repo_dest_dir)
+ # get subpath for the actual_module_name
+ if last_module_part_replaced:
+ subpath = '/'.join(tag_parts[:-1]) + '/' + module_parts[-1]
+ if not os.path.isdir(repo_dest_dir + '/' + subpath):
+ subpath = '/'.join(tag_parts[:-1])
+ else:
+ subpath = '/'.join(tag_parts[:-1])
+ if not os.path.isdir(repo_dest_dir + '/' + subpath):
+ logger.warning("subpath (%s) derived from tag matching does not exist in %s" % (subpath, repo_dest_dir))
+ return None
+ self.modules_subpaths[module_name] = subpath
+ logger.info("modules_subpath[%s] = %s" % (module_name, subpath))
+ return rev_return
+ except:
+ tag_parts.pop(0)
+ tag = '/'.join(tag_parts)
+ logger.warning("No tag matching %s" % rev)
+ return None
+
+ requiredrev = get_requiredrev(get_subpath)
+ if requiredrev:
+ logger.info("Got module(%s) requiredrev: %s" % (module_name, requiredrev))
+ if checkout:
+ subprocess.check_call('git checkout -b gomodautogen %s' % requiredrev, shell=True, cwd=repo_dest_dir)
+ self.modules_repoinfo[module_name] = (repo_url, repo_dest_dir, requiredrev)
+ else:
+ logger.warning("Failed to get requiredrev, repo_url = %s, rev = %s, module_name = %s" % (repo_url, rev, module_name))
+
+ def parse_go_mod(self, go_mod_path):
+ """
+ Parse go.mod file to get the modules info
+ """
+ # First we get the require and replace lines
+ # The parsing logic assumes the replace lines come *after* the require lines
+ inrequire = False
+ inreplace = False
+ with open(go_mod_path, 'r') as f:
+ lines = f.readlines()
+ for line in lines:
+ if line.startswith('require ('):
+ inrequire = True
+ continue
+ if line.startswith(')'):
+ inrequire = False
+ continue
+ if line.startswith('require ') or inrequire:
+ # we have one line require
+ require_line = line.lstrip('require ').split('//')[0].strip()
+ if require_line:
+ self.require_lines.append(require_line)
+ continue
+ # we can deal with requires and replaces separately because go.mod always writes requires before replaces
+ if line.startswith('replace ('):
+ inreplace = True
+ continue
+ if line.startswith(')'):
+ inreplace = False
+ continue
+ if line.startswith('replace ') or inreplace:
+ replace_line = line.lstrip('replace ').split('//')[0].strip()
+ if replace_line:
+ self.replace_lines.append(replace_line)
+ continue
+ #
+ # parse the require_lines and replace_lines to form self.modules_require and self.modules_replace
+ #
+ logger.debug("Parsing require_lines and replace_lines ...")
+ # A typical replace line is as below:
+ # github.com/hashicorp/golang-lru => github.com/ktock/golang-lru v0.5.5-0.20211029085301-ec551be6f75c
+ # It means that the github.com/hashicorp/golang-lru module is replaced by github.com/ktock/golang-lru
+ # with the version 'v0.5.5-0.20211029085301-ec551be6f75c'.
+ # So the destdir is vendor/github.com/hashicorp/golang-lru while the contents are from github.com/ktock/golang-lru
+ for line in self.replace_lines:
+ orig_module, actual = line.split('=>')
+ actual_module, actual_version = actual.split()
+ orig_module = orig_module.strip()
+ actual_module = actual_module.strip()
+ actual_version = actual_version.strip()
+ self.modules_replace[orig_module] = (actual_module, actual_version)
+ #
+ # Typical require lines are as below:
+ # github.com/Masterminds/semver/v3 v3.1.1
+ # golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
+ #
+ # We need to first try https://<module_name>?=go-get=1 to see it contains
+ # line starting with '<meta name="go-import" content='.
+ #
+ # If so, get root-path vcs repo-url from content. See https://go.dev/ref/mod#vcs-find
+ # For example, the above 'wget https://golang.org/x/crypto?go-get=1' gives you
+ # <meta name="go-import" content="golang.org/x/crypto git https://go.googlesource.com/crypto">
+ # In such case, the self.modules_require has the following contents:
+ # module_name: golang.org/x/crypto
+ # url: https://go.googlesource.com/crypto
+ # version: v0.0.0-20220321153916-2c7772ba3064
+ # destdir: ${WORKDIR}/${BP}/src/import/vendor.fetch/golang.org/x/crypto
+ # fullsrcrev: 2c7772ba30643b7a2026cbea938420dce7c6384d (git rev-list -1 2c7772ba3064)
+ #
+ # If not, try https://pkg.go.dev/<module_name>, and find the 'Repository'.
+ # For example, 'wget https://pkg.go.dev/github.com/Masterminds/semver/v3' gives:
+ # github.com/Masterminds/semver
+ # In such case, the self.modules has the following contents:
+ # module_name: github.com/Masterminds/semver/v3
+ # url: https://github.com/Masterminds/semver
+ # version: v3.1.1
+ # destdir: ${WORKDIR}/${BP}/src/import/vendor.fetch/github.com/Masterminds/semver/v3
+ # fullsrcrev: 7bb0c843b53d6ad21a3f619cb22c4b442bb3ef3e (git rev-list -1 v3.1.1)
+ #
+ # As a last resort, if the last component of <module_name> matches 'v[0-9]+',
+ # remove the last component and try wget https://<module_name_with_last_component_removed>?go-get=1,
+ # then try using the above matching method.
+ #
+ for line in self.require_lines:
+ module_name, version = line.strip().split()
+ logger.debug("require line: %s" % line)
+ logger.debug("module_name = %s; version = %s" % (module_name, version))
+ # take the modules_replace into consideration to get the actual version and actual module name
+ # note that the module_name is used in destdir, and the actual_module_name and actual_version
+ # are used to determine the url and fullsrcrev
+ destdir = '${WORKDIR}/${BP}/src/import/vendor.fetch/%s' % module_name
+ actual_module_name = module_name
+ actual_version = version
+ if module_name in self.modules_replace:
+ actual_module_name, actual_version = self.modules_replace[module_name]
+ logger.debug("actual_module_name = %s; actual_version = %s" % (actual_module_name, actual_version))
+ url, fullsrcrev = self.get_url_srcrev(actual_module_name, actual_version)
+ logger.debug("url = %s; fullsrcrev = %s" % (url, fullsrcrev))
+ if url and fullsrcrev:
+ self.modules_require[module_name] = (url, version, destdir, fullsrcrev)
+ # form srcpath, actual_module_name/<subpath>
+ if actual_module_name in self.modules_subpaths:
+ subpath = self.modules_subpaths[actual_module_name]
+ srcpath = '%s/%s' % (actual_module_name, subpath)
+ self.modules_srcpaths[module_name] = srcpath
+ logger.info("self.modules_srcpaths[%s] = %s" % (module_name, srcpath))
+ else:
+ self.modules_srcpaths[module_name] = actual_module_name
+ else:
+ logger.warning("get_url_srcrev(%s, %s) failed" % (actual_module_name, actual_version))
+ if ERROR_OUT_ON_FETCH_AND_CHECKOUT_FAILURE:
+ sys.exit(1)
+
+ def use_wget_to_get_repo_url(self, wget_content_file, url_cache_file, module_name):
+ """
+ Use wget to get repo_url for module_name, return None if not found
+ """
+ try:
+ logger.info("wget -O %s https://%s?=go-get=1" % (wget_content_file, module_name))
+ subprocess.check_call('wget -O %s https://%s?=go-get=1' % (wget_content_file, module_name), shell=True)
+ with open(wget_content_file, 'r') as f:
+ for line in f.readlines():
+ if '<meta name="go-import" content=' in line:
+ logger.info("Succeed to find go-import content for %s" % module_name)
+ logger.debug("The line is %s" % line)
+ root_path, vcs, repo_url = line.split('content=')[1].split('"')[1].split()
+ logger.info("%s: %s %s %s" % (module_name, root_path, vcs, repo_url))
+ if vcs != 'git':
+ logger.warning('%s unhandled as its vcs is %s which is not supported by this script.' % (module_name, vcs))
+ unhandled_reason = 'vcs %s is not supported by this script' % vcs
+ self.modules_unhandled[module_name] = unhandled_reason
+ return None
+ with open(url_cache_file, 'w') as f:
+ f.write(repo_url)
+ return repo_url
+ except:
+ logger.info("wget -O %s https://%s?=go-get=1 failed" % (wget_content_file, module_name))
+ # if we cannot find repo url from https://<module_name>?=go-get=1, try https://pkg.go/dev/<module_name>
+ try:
+ logger.info("wget -O %s https://pkg.go.dev/%s" % (wget_content_file, module_name))
+ subprocess.check_call("wget -O %s https://pkg.go.dev/%s" % (wget_content_file, module_name), shell=True)
+ repo_url_found = False
+ with open(wget_content_file, 'r') as f:
+ in_repo_section = False
+ for line in f.readlines():
+ if '>Repository<' in line:
+ in_repo_section = True
+ continue
+ if in_repo_section:
+ newline = line.strip()
+ if newline != '' and not newline.startswith('<'):
+ repo_url = newline
+ repo_url_found = True
+ break
+ if repo_url_found:
+ logger.info("repo url for %s: %s" % (module_name, repo_url))
+ with open(url_cache_file, 'w') as f:
+ f.write(repo_url)
+ return repo_url
+ else:
+ unhandled_reason = 'cannot determine repo_url for %s' % module_name
+ self.modules_unhandled[module_name] = unhandled_reason
+ return None
+ except:
+ logger.info("wget -O %s https://pkg.go.dev/%s failed" % (wget_content_file, module_name))
+ return None
+
+
+ def get_repo_url_rev(self, module_name, version):
+ """
+ Return (repo_url, rev)
+ """
+ import re
+ # First get rev from version
+ v = version.split('+incompatible')[0]
+ version_components = v.split('-')
+ if len(version_components) == 1:
+ rev = v
+ elif len(version_components) == 3:
+ if len(version_components[2]) == 12:
+ rev = version_components[2]
+ else:
+ rev = v
+ else:
+ rev = v
+
+ #
+ # Get repo_url
+ # We put a cache mechanism here, <wget_content_file>.repo_url.cache is used to store the repo url fetch before
+ #
+ wget_dir = os.path.join(self.workdir, 'wget-contents')
+ if not os.path.exists(wget_dir):
+ os.makedirs(wget_dir)
+ wget_content_file = os.path.join(wget_dir, module_name.replace('/', '_'))
+ url_cache_file = "%s.repo_url.cache" % wget_content_file
+ if os.path.exists(url_cache_file):
+ with open(url_cache_file, 'r') as f:
+ repo_url = f.readline().strip()
+ return (repo_url, rev)
+ module_name_parts = module_name.split('/')
+ while (len(module_name_parts) > 0):
+ module_name_to_check = '/'.join(module_name_parts)
+ logger.info("module_name_to_check: %s" % module_name_to_check)
+ repo_url = self.use_wget_to_get_repo_url(wget_content_file, url_cache_file, module_name_to_check)
+ if repo_url:
+ return (repo_url, rev)
+ else:
+ if module_name in self.modules_unhandled:
+ return (None, rev)
+ else:
+ module_name_parts.pop(-1)
+
+ unhandled_reason = 'cannot determine the repo for %s' % module_name
+ self.modules_unhandled[module_name] = unhandled_reason
+ return (None, rev)
+
+ def get_url_srcrev(self, module_name, version):
+ """
+ Return url and fullsrcrev according to module_name and version
+ """
+ repo_url, rev = self.get_repo_url_rev(module_name, version)
+ if not repo_url or not rev:
+ return (None, None)
+ self.fetch_and_checkout_repo(module_name, repo_url, rev)
+ if module_name in self.modules_repoinfo:
+ repo_url, repo_dest_dir, repo_fullrev = self.modules_repoinfo[module_name]
+ # remove the .git suffix to sync repos across modules with different versions and across recipes
+ if repo_url.endswith('.git'):
+ repo_url = repo_url[:-len('.git')]
+ return (repo_url, repo_fullrev)
+ else:
+ unhandled_reason = 'fetch_and_checkout_repo(%s, %s, %s) failed' % (module_name, repo_url, rev)
+ self.modules_unhandled[module_name] = unhandled_reason
+ return (None, None)
+
+ def gen_src_uri_inc(self):
+ """
+ Generate src_uri.inc file containing SRC_URIs
+ """
+ src_uri_inc_file = os.path.join(self.workdir, 'src_uri.inc')
+ # record the <name> after writting SRCREV_<name>, this is to avoid modules having the same basename resulting in same SRCREV_xxx
+ srcrev_name_recorded = []
+ template = """# %s %s
+# [1] git ls-remote %s %s
+SRCREV_%s="%s"
+SRC_URI += "git://%s;name=%s;protocol=https;nobranch=1;destsuffix=${WORKDIR}/${BP}/src/import/vendor.fetch/%s"
+
+"""
+ # We can't simply write SRC_URIs one by one in the order that go.mod specify them.
+ # Because the latter one might clean things up for the former one if the former one is a subpath of the latter one.
+ def take_first_len(elem):
+ return len(elem[0])
+
+ src_uri_contents = []
+ with open(src_uri_inc_file, 'w') as f:
+ for module in self.modules_require:
+ # {module_name: (url, version, destdir, fullsrcrev)}
+ repo_url, version, destdir, fullrev = self.modules_require[module]
+ if module in self.modules_replace:
+ actual_module_name, actual_version = self.modules_replace[module]
+ else:
+ actual_module_name, actual_version = (module, version)
+ if '://' in repo_url:
+ repo_url_noprotocol = repo_url.split('://')[1]
+ else:
+ repo_url_noprotocol = repo_url
+ if not repo_url.startswith('https://'):
+ repo_url = 'https://' + repo_url
+ name = module.split('/')[-1]
+ if name in srcrev_name_recorded:
+ name = '-'.join(module.split('/')[-2:])
+ src_uri_contents.append((actual_module_name, actual_version, repo_url, fullrev, name, fullrev, repo_url_noprotocol, name, actual_module_name))
+ srcrev_name_recorded.append(name)
+ # sort the src_uri_contents and then write it
+ src_uri_contents.sort(key=take_first_len)
+ for content in src_uri_contents:
+ f.write(template % content)
+ logger.info("%s generated" % src_uri_inc_file)
+
+ def gen_relocation_inc(self):
+ """
+ Generate relocation.inc file
+ """
+ relocation_inc_file = os.path.join(self.workdir, 'relocation.inc')
+ template = """export sites="%s"
+
+do_compile:prepend() {
+ cd ${S}/src/import
+ for s in $sites; do
+ site_dest=$(echo $s | cut -d: -f1)
+ site_source=$(echo $s | cut -d: -f2)
+ force_flag=$(echo $s | cut -d: -f3)
+ mkdir -p vendor.copy/$site_dest
+ if [ -n "$force_flag" ]; then
+ echo "[INFO] $site_dest: force copying .go files"
+ rm -rf vendor.copy/$site_dest
+ rsync -a --exclude='vendor/' --exclude='.git/' vendor.fetch/$site_source/ vendor.copy/$site_dest
+ else
+ [ -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 ; }
+ fi
+ done
+}
+"""
+ sites = []
+ for module in self.modules_require:
+ # <dest>:<source>[:force]
+ if module in self.modules_srcpaths:
+ srcpath = self.modules_srcpaths[module]
+ logger.debug("Using %s as srcpath of module (%s)" % (srcpath, module))
+ else:
+ srcpath = module
+ sites.append("%s:%s:force" % (module, srcpath))
+ # To avoid the former one being overriden by the latter one when the former one is a subpath of the latter one, sort sites
+ sites.sort(key=len)
+ with open(relocation_inc_file, 'w') as f:
+ sites_str = ' \\\n '.join(sites)
+ f.write(template % sites_str)
+ logger.info("%s generated" % relocation_inc_file)
+
+ def gen_modules_txt(self):
+ """
+ Generate modules.txt file
+ """
+ modules_txt_file = os.path.join(self.workdir, 'modules.txt')
+ with open(modules_txt_file, 'w') as f:
+ for l in self.require_lines:
+ f.write('# %s\n' % l)
+ f.write('## explicit\n')
+ for l in self.replace_lines:
+ f.write('# %s\n' %l)
+ logger.info("%s generated" % modules_txt_file)
+
+ def sanity_check(self):
+ """
+ Various anity checks
+ """
+ sanity_check_ok = True
+ #
+ # Sanity Check 1:
+ # For modules having the same repo, at most one is allowed to not have subpath.
+ # This check operates on self.modules_repoinfo and self.modules_subpaths
+ #
+ repo_modules = {}
+ for module in self.modules_repoinfo:
+ # first form {repo: [module1, module2, ...]}
+ repo_url, repo_dest_dir, fullsrcrev = self.modules_repoinfo[module]
+ if repo_url not in repo_modules:
+ repo_modules[repo_url] = [module]
+ else:
+ repo_modules[repo_url].append(module)
+ for repo in repo_modules:
+ modules = repo_modules[repo]
+ if len(modules) == 1:
+ continue
+ # for modules sharing the same repo, at most one is allowed to not have subpath
+ nosubpath_modules = []
+ for m in modules:
+ if m not in self.modules_subpaths:
+ nosubpath_modules.append(m)
+ if len(nosubpath_modules) == 0:
+ continue
+ if len(nosubpath_modules) > 1:
+ logger.warning("Multiple modules sharing %s, but they don't have subpath: %s. Please double check." % (repo, nosubpath_modules))
+ if len(nosubpath_modules) == 1:
+ # do further check, OK if the module is the prefix for other modules sharing the same repo
+ module_to_check = nosubpath_modules[0]
+ for m in modules:
+ if module_to_check == m:
+ continue
+ if not m.startswith('%s/' % module_to_check):
+ logger.warning("%s is sharing repo (%s) with other modules, and it might need a subpath. Please double check: %s and: %s" % (module_to_check, repo, nosubpath_modules,m))
+ continue
+
+ #
+ # End of Sanity Check
+ #
+ if not sanity_check_ok:
+ sys.exit(1)
+ return
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="go mod dependency -> SRC_URI procesing",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=textwrap.dedent('''\
+
+ Overview:
+ =========
+
+ go-mod-oe is a tool for processing go dependencies to generate
+ dependencies suitable for OE fetcher consumption.
+
+ In particular, it creates a build structure suitable for
+ '-mod="vendor"' go builds. Once complete all go mod dependencies
+ are in the vendor/ directory, so no golang specific fetching or
+ network access happens during the build.
+
+ The files src_uri.inc, relocation.inc and modules.txt are generated
+ and suitable for recipe inclusion.
+
+ A recipe build can then use these files to leverage the git fetcher
+ and related functionality (mirrors, sstate, etc).
+
+ Note 1: --rev does not have to be a tag, if you want to track the tip of
+ a branch specify the latest git has on that branch, and it will
+ be used.
+
+ Note 2: This script does not generate an entire recipe, the way the
+ the outputs are used can be modified as required.
+
+ Note 3: if a go.mod has a bad revision, or needs to be manually updated
+ to fetch fixes: go.mod in the main repository (see the repos/
+ directory). If go.mod is edited, modules.txt also has to be
+ updated to match the revision information.
+
+ How to use in a recipe:
+ =======================
+
+ There are examples in meta-virtualization of recipes that use this
+ script and stragegy for builds: docker-compose, nerdcli, k3s
+
+ 1) The recipe should set the master repository SRCREV details, and then include
+ the src_uri.inc file:
+
+ SRCREV_nerdcli = "e084a2df4a8861eb5f0b0d32df0643ef24b81093"
+ SRC_URI = "git://github.com/containerd/nerdctl.git;name=nerdcli;branch=master;protocol=https"
+
+ include src_uri.inc
+
+ This results in the SRC_URI being fully populated with the main
+ repository and all dependencies.
+
+ 2) The recipe should either copy, or include the relocation.inc file. It sets
+ a variable "sites" that is a list of source locations (where the src_uri.inc
+ fetches) and destination in a vendor directory, it also has a do_compile:prepend()
+ that contains a loop which relocates the fetches into a vendor.copy directory.
+
+ It is expected to be processed as follows, before compilation starts:
+
+ # sets the "sites" variable and copies files
+ include relocation.inc
+
+ The do_compile:prepend, contains the following loop:
+
+ cd ${S}/src/import
+ # this moves all the fetches into the proper vendor structure
+ # expected for build
+ for s in ${sites}; do
+ site_dest=$(echo $s | cut -d: -f1)
+ site_source=$(echo $s | cut -d: -f2)
+ force_flag=$(echo $s | cut -d: -f3)
+ mkdir -p vendor.copy/$site_dest
+ if [ -n "$force_flag" ]; then
+ echo "[INFO] $site_dest: force copying .go files"
+ rm -rf vendor.copy/$site_dest
+ rsync -a --exclude='vendor/' --exclude='.git/' vendor.fetch/$site_source/ vendor.copy/$site_dest
+ else
+ [ -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 ; }
+ fi
+ done
+
+ The main compile() function, should set the appropriate GO variables,
+ copy modules.txt and build the appripriate target:
+
+ # our copied .go files are to be used for the build
+ ln -sf vendor.copy vendor
+
+ 3) The modules.txt file should be copied into the recipe directory, included
+ on the SRC_URI and copied into place after the relocation has been
+ processed.
+
+ # patches and config
+ SRC_URI += "file://0001-Makefile-allow-external-specification-of-build-setti.patch \\
+ file://modules.txt \
+ "
+
+ .....
+
+ cp ${WORKDIR}/modules.txt vendor/
+
+ Example: Updating the K3S recipe
+ ================================
+
+ % cd meta-virtualization/recipe-containers/k3s/
+ # produces src_uri.inc, relocation.inc and modules.txt in the current directory
+ % ../../scripts/oe-go-mod-autogen.py --repo https://github.com/rancher/k3s.git --rev v1.27.5+k3s1
+
+ % cp modules.txt k3s/
+
+ ... add and commit files.
+
+
+ '''))
+ parser.add_argument("--repo", help = "Repo for the recipe.", required=True)
+ parser.add_argument("--rev", help = "Revision for the recipe.", required=True)
+ parser.add_argument("--module", help = "Go module name. To be used with '--test'")
+ parser.add_argument("--version", help = "Go module version. To be used with '--test'")
+ parser.add_argument("--test", help = "Test to get repo url and fullsrcrev, used together with --module and --version.", action="store_true")
+ parser.add_argument("--workdir", help = "Working directory to hold intermediate results and output.", default=os.getcwd())
+ parser.add_argument("-d", "--debug",
+ help = "Enable debug output",
+ action="store_const", const=logging.DEBUG, dest="loglevel", default=logging.INFO)
+ parser.add_argument("-q", "--quiet",
+ help = "Hide all output except error messages",
+ action="store_const", const=logging.ERROR, dest="loglevel")
+ parser.add_argument("-v", action='store_true', dest="verbose",
+ help="verbose")
+
+ args = parser.parse_args()
+
+ if args.verbose:
+ args.loglevel = args.verbose
+ args = parser.parse_args()
+
+ logger.setLevel(args.loglevel)
+ logger.debug("oe-go-mod-autogen.py running for %s:%s in %s" % (args.repo, args.rev, args.workdir))
+ gomodtool = GoModTool(args.repo, args.rev, args.workdir)
+ if args.test:
+ if not args.module or not args.version:
+ print("Please specify --module and --version")
+ sys.exit(1)
+ url, srcrev = gomodtool.get_url_srcrev(args.module, args.version)
+ print("url = %s, srcrev = %s" % (url, srcrev))
+ if not url or not srcrev:
+ print("Failed to get url & srcrev for %s:%s" % (args.module, args.version))
+ else:
+ gomodtool.parse()
+ gomodtool.sanity_check()
+ gomodtool.gen_src_uri_inc()
+ gomodtool.gen_relocation_inc()
+ gomodtool.gen_modules_txt()
+
+
+if __name__ == "__main__":
+ try:
+ ret = main()
+ except Exception as esc:
+ ret = 1
+ import traceback
+ traceback.print_exc()
+ sys.exit(ret)