| # |
| # 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 |