diff options
Diffstat (limited to 'meta/classes/go-vendor.bbclass')
-rw-r--r-- | meta/classes/go-vendor.bbclass | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/meta/classes/go-vendor.bbclass b/meta/classes/go-vendor.bbclass new file mode 100644 index 0000000000..1bbb99ac79 --- /dev/null +++ b/meta/classes/go-vendor.bbclass @@ -0,0 +1,211 @@ +# +# Copyright 2023 (C) Weidmueller GmbH & Co KG +# Author: Lukas Funke <lukas.funke@weidmueller.com> +# +# Handle Go vendor support for offline builds +# +# When importing Go modules, Go downloads the imported modules using +# a network (proxy) connection ahead of the compile stage. This contradicts +# the yocto build concept of fetching every source ahead of build-time +# and supporting offline builds. +# +# To support offline builds, we use Go 'vendoring': module dependencies are +# downloaded during the fetch-phase and unpacked into the modules 'vendor' +# folder. Additionally a manifest file is generated for the 'vendor' folder +# + +inherit go-mod + +def go_src_uri(repo, version, path=None, subdir=None, \ + vcs='git', replaces=None, pathmajor=None): + + destsuffix = "git/src/import/vendor.fetch" + module_path = repo if not path else path + + src_uri = "{}://{};name={}".format(vcs, repo, module_path.replace('/', '.')) + src_uri += ";destsuffix={}/{}@{}".format(destsuffix, repo, version) + + if vcs == "git": + src_uri += ";nobranch=1;protocol=https" + + src_uri += ";go_module_path={}".format(module_path) + + if replaces: + src_uri += ";go_module_replacement={}".format(replaces) + if subdir: + src_uri += ";go_subdir={}".format(subdir) + if pathmajor: + src_uri += ";go_pathmajor={}".format(pathmajor) + src_uri += ";is_go_dependency=1" + + return src_uri + +python do_vendor_unlink() { + go_import = d.getVar('GO_IMPORT') + source_dir = d.getVar('S') + linkname = os.path.join(source_dir, *['src', go_import, 'vendor']) + + os.unlink(linkname) +} + +addtask vendor_unlink before do_package after do_install + +python do_go_vendor() { + import shutil + + src_uri = (d.getVar('SRC_URI') or "").split() + + if not src_uri: + bb.fatal("SRC_URI is empty") + + default_destsuffix = "git/src/import/vendor.fetch" + fetcher = bb.fetch2.Fetch(src_uri, d) + go_import = d.getVar('GO_IMPORT') + source_dir = d.getVar('S') + + linkname = os.path.join(source_dir, *['src', go_import, 'vendor']) + vendor_dir = os.path.join(source_dir, *['src', 'import', 'vendor']) + import_dir = os.path.join(source_dir, *['src', 'import', 'vendor.fetch']) + + if os.path.exists(vendor_dir): + # Nothing to do except re-establish link to actual vendor folder + if not os.path.exists(linkname): + os.symlink(vendor_dir, linkname) + return + + bb.utils.mkdirhier(vendor_dir) + + modules = {} + + for url in fetcher.urls: + srcuri = fetcher.ud[url].host + fetcher.ud[url].path + + # Skip non Go module src uris + if not fetcher.ud[url].parm.get('is_go_dependency'): + continue + + destsuffix = fetcher.ud[url].parm.get('destsuffix') + # We derive the module repo / version in the following manner (exmaple): + # + # destsuffix = git/src/import/vendor.fetch/github.com/foo/bar@v1.2.3 + # p = github.com/foo/bar@v1.2.3 + # repo = github.com/foo/bar + # version = v1.2.3 + + p = destsuffix[len(default_destsuffix)+1:] + repo, version = p.split('@') + + module_path = fetcher.ud[url].parm.get('go_module_path') + + subdir = fetcher.ud[url].parm.get('go_subdir') + subdir = None if not subdir else subdir + + pathMajor = fetcher.ud[url].parm.get('go_pathmajor') + pathMajor = None if not pathMajor else pathMajor.strip('/') + + if not (repo, version) in modules: + modules[(repo, version)] = { + "repo_path": os.path.join(import_dir, p), + "module_path": module_path, + "subdir": subdir, + "pathMajor": pathMajor } + + for module_key, module in modules.items(): + + # only take the version which is explicitly listed + # as a dependency in the go.mod + module_path = module['module_path'] + rootdir = module['repo_path'] + subdir = module['subdir'] + pathMajor = module['pathMajor'] + + src = rootdir + + if subdir: + src = os.path.join(rootdir, subdir) + + # If the module is released at major version 2 or higher, the module + # path must end with a major version suffix like /v2. + # This may or may not be part of the subdirectory name + # + # https://go.dev/ref/mod#modules-overview + if pathMajor: + tmp = os.path.join(src, pathMajor) + # source directory including major version path may or may not exist + if os.path.exists(tmp): + src = tmp + + dst = os.path.join(vendor_dir, module_path) + + bb.debug(1, "cp %s --> %s" % (src, dst)) + shutil.copytree(src, dst, symlinks=True, dirs_exist_ok=True, \ + ignore=shutil.ignore_patterns(".git", \ + "vendor", \ + "*._test.go")) + + # If the root directory has a LICENSE file but not the subdir + # we copy the root license to the sub module since the license + # applies to all modules in the repository + # see https://go.dev/ref/mod#vcs-license + if subdir: + rootdirLicese = os.path.join(rootdir, "LICENSE") + subdirLicense = os.path.join(src, "LICENSE") + + if not os.path.exists(subdir) and \ + os.path.exists(rootdirLicese): + shutil.copy2(rootdirLicese, subdirLicense) + + # Copy vendor manifest + modules_txt_src = os.path.join(d.getVar('WORKDIR'), "modules.txt") + bb.debug(1, "cp %s --> %s" % (modules_txt_src, vendor_dir)) + shutil.copy2(modules_txt_src, vendor_dir) + + # Clean up vendor dir + # We only require the modules in the modules_txt file + fetched_paths = set([os.path.relpath(x[0], vendor_dir) for x in os.walk(vendor_dir)]) + + # Remove toplevel dir + fetched_paths.remove('.') + + vendored_paths = set() + replaced_paths = dict() + with open(modules_txt_src) as f: + for line in f: + if not line.startswith("#"): + line = line.strip() + vendored_paths.add(line) + + # Add toplevel dirs into vendored dir, as we want to keep them + topdir = os.path.dirname(line) + while len(topdir): + if not topdir in vendored_paths: + vendored_paths.add(topdir) + + topdir = os.path.dirname(topdir) + else: + replaced_module = line.split("=>") + if len(replaced_module) > 1: + # This module has been replaced, use a local path + # we parse the line that has a pattern "# module-name [module-version] => local-path + actual_path = replaced_module[1].strip() + vendored_name = replaced_module[0].split()[1] + bb.debug(1, "added vendored name %s for actual path %s" % (vendored_name, actual_path)) + replaced_paths[vendored_name] = actual_path + + for path in fetched_paths: + if path not in vendored_paths: + realpath = os.path.join(vendor_dir, path) + if os.path.exists(realpath): + shutil.rmtree(realpath) + + for vendored_name, replaced_path in replaced_paths.items(): + symlink_target = os.path.join(source_dir, *['src', go_import, replaced_path]) + symlink_name = os.path.join(vendor_dir, vendored_name) + bb.debug(1, "vendored name %s, symlink name %s" % (vendored_name, symlink_name)) + os.symlink(symlink_target, symlink_name) + + # Create a symlink to the actual directory + os.symlink(vendor_dir, linkname) +} + +addtask go_vendor before do_patch after do_unpack |