diff mbox series

[v3,4/4] recipetool: Add handler to create go recipes

Message ID 20231102155313.309534-5-lukas.funke-oss@weidmueller.com
State Accepted, archived
Commit 049afb933f42851d56d2e6522150a32bcccf2a6f
Headers show
Series recipetool: Add handler to create go recipes | expand

Commit Message

Lukas Funke Nov. 2, 2023, 3:53 p.m. UTC
From: Lukas Funke <lukas.funke@weidmueller.com>

Signed-off-by: Lukas Funke <lukas.funke@weidmueller.com>
---
 scripts/lib/recipetool/create_go.py | 751 ++++++++++++++++++++++++++++
 1 file changed, 751 insertions(+)
 create mode 100644 scripts/lib/recipetool/create_go.py
diff mbox series

Patch

diff --git a/scripts/lib/recipetool/create_go.py b/scripts/lib/recipetool/create_go.py
new file mode 100644
index 0000000000..c7b2381a33
--- /dev/null
+++ b/scripts/lib/recipetool/create_go.py
@@ -0,0 +1,751 @@ 
+# Recipe creation tool - go support plugin
+#
+# The code is based on golang internals. See the afftected
+# methods for further reference and information.
+#
+# Copyright (C) 2023 Weidmueller GmbH & Co KG
+# Author: Lukas Funke <lukas.funke@weidmueller.com>
+#
+# SPDX-License-Identifier: GPL-2.0-only
+#
+
+
+from collections import namedtuple
+from enum import Enum
+from html.parser import HTMLParser
+from recipetool.create import RecipeHandler, handle_license_vars
+from recipetool.create import guess_license, tidy_licenses, fixup_license
+from recipetool.create import determine_from_url
+from urllib.error import URLError
+
+import bb.utils
+import json
+import logging
+import os
+import re
+import subprocess
+import sys
+import shutil
+import tempfile
+import urllib.parse
+import urllib.request
+
+
+GoImport = namedtuple('GoImport', 'root vcs url suffix')
+logger = logging.getLogger('recipetool')
+CodeRepo = namedtuple(
+    'CodeRepo', 'path codeRoot codeDir pathMajor pathPrefix pseudoMajor')
+
+tinfoil = None
+
+# Regular expression to parse pseudo semantic version
+# see https://go.dev/ref/mod#pseudo-versions
+re_pseudo_semver = re.compile(
+    r"^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)(?P<utc>\d{14})-(?P<commithash>[A-Za-z0-9]+)(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$")
+# Regular expression to parse semantic version
+re_semver = re.compile(
+    r"^v(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")
+
+
+def tinfoil_init(instance):
+    global tinfoil
+    tinfoil = instance
+
+
+class GoRecipeHandler(RecipeHandler):
+    """Class to handle the go recipe creation"""
+
+    @staticmethod
+    def __ensure_go():
+        """Check if the 'go' command is available in the recipes"""
+        recipe = "go-native"
+        if not tinfoil.recipes_parsed:
+            tinfoil.parse_recipes()
+        try:
+            rd = tinfoil.parse_recipe(recipe)
+        except bb.providers.NoProvider:
+            bb.error(
+                "Nothing provides '%s' which is required for the build" % (recipe))
+            bb.note(
+                "You will likely need to add a layer that provides '%s'" % (recipe))
+            return None
+
+        bindir = rd.getVar('STAGING_BINDIR_NATIVE')
+        gopath = os.path.join(bindir, 'go')
+
+        if not os.path.exists(gopath):
+            tinfoil.build_targets(recipe, 'addto_recipe_sysroot')
+
+            if not os.path.exists(gopath):
+                logger.error(
+                    '%s required to process specified source, but %s did not seem to populate it' % 'go', recipe)
+                return None
+
+        return bindir
+
+    def __resolve_repository_static(self, modulepath):
+        """Resolve the repository in a static manner
+
+            The method is based on the go implementation of
+            `repoRootFromVCSPaths` in
+            https://github.com/golang/go/blob/master/src/cmd/go/internal/vcs/vcs.go
+        """
+
+        url = urllib.parse.urlparse("https://" + modulepath)
+        req = urllib.request.Request(url.geturl())
+
+        try:
+            resp = urllib.request.urlopen(req)
+            # Some modulepath are just redirects to github (or some other vcs
+            # hoster). Therefore, we check if this modulepath redirects to
+            # somewhere else
+            if resp.geturl() != url.geturl():
+                bb.debug(1, "%s is redirectred to %s" %
+                         (url.geturl(), resp.geturl()))
+                url = urllib.parse.urlparse(resp.geturl())
+                modulepath = url.netloc + url.path
+
+        except URLError as url_err:
+            # This is probably because the module path
+            # contains the subdir and major path. Thus,
+            # we ignore this error for now
+            logger.debug(
+                1, "Failed to fetch page from [%s]: %s" % (url, str(url_err)))
+
+        host, _, _ = modulepath.partition('/')
+
+        class vcs(Enum):
+            pathprefix = "pathprefix"
+            regexp = "regexp"
+            type = "type"
+            repo = "repo"
+            check = "check"
+            schemelessRepo = "schemelessRepo"
+
+        # GitHub
+        vcsGitHub = {}
+        vcsGitHub[vcs.pathprefix] = "github.com"
+        vcsGitHub[vcs.regexp] = re.compile(
+            r'^(?P<root>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/(?P<suffix>[A-Za-z0-9_.\-]+))*$')
+        vcsGitHub[vcs.type] = "git"
+        vcsGitHub[vcs.repo] = "https://\\g<root>"
+
+        # Bitbucket
+        vcsBitbucket = {}
+        vcsBitbucket[vcs.pathprefix] = "bitbucket.org"
+        vcsBitbucket[vcs.regexp] = re.compile(
+            r'^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/(?P<suffix>[A-Za-z0-9_.\-]+))*$')
+        vcsBitbucket[vcs.type] = "git"
+        vcsBitbucket[vcs.repo] = "https://\\g<root>"
+
+        # IBM DevOps Services (JazzHub)
+        vcsIBMDevOps = {}
+        vcsIBMDevOps[vcs.pathprefix] = "hub.jazz.net/git"
+        vcsIBMDevOps[vcs.regexp] = re.compile(
+            r'^(?P<root>hub\.jazz\.net/git/[a-z0-9]+/[A-Za-z0-9_.\-]+)(/(?P<suffix>[A-Za-z0-9_.\-]+))*$')
+        vcsIBMDevOps[vcs.type] = "git"
+        vcsIBMDevOps[vcs.repo] = "https://\\g<root>"
+
+        # Git at Apache
+        vcsApacheGit = {}
+        vcsApacheGit[vcs.pathprefix] = "git.apache.org"
+        vcsApacheGit[vcs.regexp] = re.compile(
+            r'^(?P<root>git\.apache\.org/[a-z0-9_.\-]+\.git)(/(?P<suffix>[A-Za-z0-9_.\-]+))*$')
+        vcsApacheGit[vcs.type] = "git"
+        vcsApacheGit[vcs.repo] = "https://\\g<root>"
+
+        # Git at OpenStack
+        vcsOpenStackGit = {}
+        vcsOpenStackGit[vcs.pathprefix] = "git.openstack.org"
+        vcsOpenStackGit[vcs.regexp] = re.compile(
+            r'^(?P<root>git\.openstack\.org/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(\.git)?(/(?P<suffix>[A-Za-z0-9_.\-]+))*$')
+        vcsOpenStackGit[vcs.type] = "git"
+        vcsOpenStackGit[vcs.repo] = "https://\\g<root>"
+
+        # chiselapp.com for fossil
+        vcsChiselapp = {}
+        vcsChiselapp[vcs.pathprefix] = "chiselapp.com"
+        vcsChiselapp[vcs.regexp] = re.compile(
+            r'^(?P<root>chiselapp\.com/user/[A-Za-z0-9]+/repository/[A-Za-z0-9_.\-]+)$')
+        vcsChiselapp[vcs.type] = "fossil"
+        vcsChiselapp[vcs.repo] = "https://\\g<root>"
+
+        # General syntax for any server.
+        # Must be last.
+        vcsGeneralServer = {}
+        vcsGeneralServer[vcs.regexp] = re.compile(
+            "(?P<root>(?P<repo>([a-z0-9.\\-]+\\.)+[a-z0-9.\\-]+(:[0-9]+)?(/~?[A-Za-z0-9_.\\-]+)+?)\\.(?P<vcs>bzr|fossil|git|hg|svn))(/~?(?P<suffix>[A-Za-z0-9_.\\-]+))*$")
+        vcsGeneralServer[vcs.schemelessRepo] = True
+
+        vcsPaths = [vcsGitHub, vcsBitbucket, vcsIBMDevOps,
+                    vcsApacheGit, vcsOpenStackGit, vcsChiselapp,
+                    vcsGeneralServer]
+
+        if modulepath.startswith("example.net") or modulepath == "rsc.io":
+            logger.warning("Suspicious module path %s" % modulepath)
+            return None
+        if modulepath.startswith("http:") or modulepath.startswith("https:"):
+            logger.warning("Import path should not start with %s %s" %
+                           ("http", "https"))
+            return None
+
+        rootpath = None
+        vcstype = None
+        repourl = None
+        suffix = None
+
+        for srv in vcsPaths:
+            m = srv[vcs.regexp].match(modulepath)
+            if vcs.pathprefix in srv:
+                if host == srv[vcs.pathprefix]:
+                    rootpath = m.group('root')
+                    vcstype = srv[vcs.type]
+                    repourl = m.expand(srv[vcs.repo])
+                    suffix = m.group('suffix')
+                    break
+            elif m and srv[vcs.schemelessRepo]:
+                rootpath = m.group('root')
+                vcstype = m[vcs.type]
+                repourl = m[vcs.repo]
+                suffix = m.group('suffix')
+                break
+
+        return GoImport(rootpath, vcstype, repourl, suffix)
+
+    def __resolve_repository_dynamic(self, modulepath):
+        """Resolve the repository root in a dynamic manner.
+
+            The method is based on the go implementation of
+            `repoRootForImportDynamic` in
+            https://github.com/golang/go/blob/master/src/cmd/go/internal/vcs/vcs.go
+        """
+        url = urllib.parse.urlparse("https://" + modulepath)
+
+        class GoImportHTMLParser(HTMLParser):
+
+            def __init__(self):
+                super().__init__()
+                self.__srv = []
+
+            def handle_starttag(self, tag, attrs):
+                if tag == 'meta' and list(
+                        filter(lambda a: (a[0] == 'name' and a[1] == 'go-import'), attrs)):
+                    content = list(
+                        filter(lambda a: (a[0] == 'content'), attrs))
+                    if content:
+                        self.__srv = content[0][1].split()
+
+            @property
+            def import_prefix(self):
+                return self.__srv[0] if len(self.__srv) else None
+
+            @property
+            def vcs(self):
+                return self.__srv[1] if len(self.__srv) else None
+
+            @property
+            def repourl(self):
+                return self.__srv[2] if len(self.__srv) else None
+
+        url = url.geturl() + "?go-get=1"
+        req = urllib.request.Request(url)
+
+        try:
+            resp = urllib.request.urlopen(req)
+
+        except URLError as url_err:
+            logger.warning(
+                "Failed to fetch page from [%s]: %s", url, str(url_err))
+            return None
+
+        parser = GoImportHTMLParser()
+        parser.feed(resp.read().decode('utf-8'))
+        parser.close()
+
+        return GoImport(parser.import_prefix, parser.vcs, parser.repourl, None)
+
+    def __resolve_from_golang_proxy(self, modulepath, version):
+        """
+        Resolves repository data from golang proxy
+        """
+        url = urllib.parse.urlparse("https://proxy.golang.org/"
+                                    + modulepath
+                                    + "/@v/"
+                                    + version
+                                    + ".info")
+
+        # Transform url to lower case, golang proxy doesn't like mixed case
+        req = urllib.request.Request(url.geturl().lower())
+
+        try:
+            resp = urllib.request.urlopen(req)
+        except URLError as url_err:
+            logger.warning(
+                "Failed to fetch page from [%s]: %s", url, str(url_err))
+            return None
+
+        golang_proxy_res = resp.read().decode('utf-8')
+        modinfo = json.loads(golang_proxy_res)
+
+        if modinfo and 'Origin' in modinfo:
+            origin = modinfo['Origin']
+            _root_url = urllib.parse.urlparse(origin['URL'])
+
+            # We normalize the repo URL since we don't want the scheme in it
+            _subdir = origin['Subdir'] if 'Subdir' in origin else None
+            _root, _, _ = self.__split_path_version(modulepath)
+            if _subdir:
+                _root = _root[:-len(_subdir)].strip('/')
+
+            _commit = origin['Hash']
+            _vcs = origin['VCS']
+            return (GoImport(_root, _vcs, _root_url.geturl(), None), _commit)
+
+        return None
+
+    def __resolve_repository(self, modulepath):
+        """
+        Resolves src uri from go module-path
+        """
+        repodata = self.__resolve_repository_static(modulepath)
+        if not repodata or not repodata.url:
+            repodata = self.__resolve_repository_dynamic(modulepath)
+            if not repodata or not repodata.url:
+                logger.error(
+                    "Could not resolve repository for module path '%s'" % modulepath)
+                # There is no way to recover from this
+                sys.exit(14)
+        if repodata:
+            logger.debug(1, "Resolved download path for import '%s' => %s" % (
+                modulepath, repodata.url))
+        return repodata
+
+    def __split_path_version(self, path):
+        i = len(path)
+        dot = False
+        for j in range(i, 0, -1):
+            if path[j - 1] < '0' or path[j - 1] > '9':
+                break
+            if path[j - 1] == '.':
+                dot = True
+                break
+            i = j - 1
+
+        if i <= 1 or i == len(
+                path) or path[i - 1] != 'v' or path[i - 2] != '/':
+            return path, "", True
+
+        prefix, pathMajor = path[:i - 2], path[i - 2:]
+        if dot or len(
+                pathMajor) <= 2 or pathMajor[2] == '0' or pathMajor == "/v1":
+            return path, "", False
+
+        return prefix, pathMajor, True
+
+    def __get_path_major(self, pathMajor):
+        if not pathMajor:
+            return ""
+
+        if pathMajor[0] != '/' and pathMajor[0] != '.':
+            logger.error(
+                "pathMajor suffix %s passed to PathMajorPrefix lacks separator", pathMajor)
+
+        if pathMajor.startswith(".v") and pathMajor.endswith("-unstable"):
+            pathMajor = pathMajor[:len("-unstable") - 2]
+
+        return pathMajor[1:]
+
+    def __build_coderepo(self, repo, path):
+        codedir = ""
+        pathprefix, pathMajor, _ = self.__split_path_version(path)
+        if repo.root == path:
+            pathprefix = path
+        elif path.startswith(repo.root):
+            codedir = pathprefix[len(repo.root):].strip('/')
+
+        pseudoMajor = self.__get_path_major(pathMajor)
+
+        logger.debug("root='%s', codedir='%s', prefix='%s', pathMajor='%s', pseudoMajor='%s'",
+                     repo.root, codedir, pathprefix, pathMajor, pseudoMajor)
+
+        return CodeRepo(path, repo.root, codedir,
+                        pathMajor, pathprefix, pseudoMajor)
+
+    def __resolve_version(self, repo, path, version):
+        hash = None
+        coderoot = self.__build_coderepo(repo, path)
+
+        def vcs_fetch_all():
+            tmpdir = tempfile.mkdtemp()
+            clone_cmd = "%s clone --bare %s %s" % ('git', repo.url, tmpdir)
+            bb.process.run(clone_cmd)
+            log_cmd = "git log --all --pretty='%H %d' --decorate=short"
+            output, _ = bb.process.run(
+                log_cmd, shell=True, stderr=subprocess.PIPE, cwd=tmpdir)
+            bb.utils.prunedir(tmpdir)
+            return output.strip().split('\n')
+
+        def vcs_fetch_remote(tag):
+            # add * to grab ^{}
+            refs = {}
+            ls_remote_cmd = "git ls-remote -q --tags {} {}*".format(
+                repo.url, tag)
+            output, _ = bb.process.run(ls_remote_cmd)
+            output = output.strip().split('\n')
+            for line in output:
+                f = line.split(maxsplit=1)
+                if len(f) != 2:
+                    continue
+
+                for prefix in ["HEAD", "refs/heads/", "refs/tags/"]:
+                    if f[1].startswith(prefix):
+                        refs[f[1].removeprefix(prefix)] = f[0]
+
+            for key, hash in refs.items():
+                if key.endswith(r"^{}"):
+                    refs[key.strip(r"^{}")] = hash
+
+            return refs[tag]
+
+        m_pseudo_semver = re_pseudo_semver.match(version)
+
+        if m_pseudo_semver:
+            remote_refs = vcs_fetch_all()
+            short_commit = m_pseudo_semver.group('commithash')
+            for l in remote_refs:
+                r = l.split(maxsplit=1)
+                sha1 = r[0] if len(r) else None
+                if not sha1:
+                    logger.error(
+                        "Ups: could not resolve abbref commit for %s" % short_commit)
+
+                elif sha1.startswith(short_commit):
+                    hash = sha1
+                    break
+        else:
+            m_semver = re_semver.match(version)
+            if m_semver:
+
+                def get_sha1_remote(re):
+                    rsha1 = None
+                    for line in remote_refs:
+                        # Split lines of the following format:
+                        # 22e90d9b964610628c10f673ca5f85b8c2a2ca9a  (tag: sometag)
+                        lineparts = line.split(maxsplit=1)
+                        sha1 = lineparts[0] if len(lineparts) else None
+                        refstring = lineparts[1] if len(
+                            lineparts) == 2 else None
+                        if refstring:
+                            # Normalize tag string and split in case of multiple
+                            # regs e.g. (tag: speech/v1.10.0, tag: orchestration/v1.5.0 ...)
+                            refs = refstring.strip('(), ').split(',')
+                            for ref in refs:
+                                if re.match(ref.strip()):
+                                    rsha1 = sha1
+                    return rsha1
+
+                semver = "v" + m_semver.group('major') + "."\
+                             + m_semver.group('minor') + "."\
+                             + m_semver.group('patch') \
+                             + (("-" + m_semver.group('prerelease'))
+                                if m_semver.group('prerelease') else "")
+
+                tag = os.path.join(
+                    coderoot.codeDir, semver) if coderoot.codeDir else semver
+
+                # probe tag using 'ls-remote', which is faster than fetching
+                # complete history
+                hash = vcs_fetch_remote(tag)
+                if not hash:
+                    # backup: fetch complete history
+                    remote_refs = vcs_fetch_all()
+                    hash = get_sha1_remote(
+                        re.compile(fr"(tag:|HEAD ->) ({tag})"))
+
+                logger.debug(
+                    "Resolving commit for tag '%s' -> '%s'", tag, hash)
+        return hash
+
+    def __generate_srcuri_inline_fcn(self, path, version, replaces=None):
+        """Generate SRC_URI functions for go imports"""
+
+        logger.info("Resolving repository for module %s", path)
+        # First try to resolve repo and commit from golang proxy
+        # Most info is already there and we don't have to go through the
+        # repository or even perform the version resolve magic
+        golang_proxy_info = self.__resolve_from_golang_proxy(path, version)
+        if golang_proxy_info:
+            repo = golang_proxy_info[0]
+            commit = golang_proxy_info[1]
+        else:
+            # Fallback
+            # Resolve repository by 'hand'
+            repo = self.__resolve_repository(path)
+            commit = self.__resolve_version(repo, path, version)
+
+        url = urllib.parse.urlparse(repo.url)
+        repo_url = url.netloc + url.path
+
+        coderoot = self.__build_coderepo(repo, path)
+
+        inline_fcn = "${@go_src_uri("
+        inline_fcn += f"'{repo_url}','{version}'"
+        if repo_url != path:
+            inline_fcn += f",path='{path}'"
+        if coderoot.codeDir:
+            inline_fcn += f",subdir='{coderoot.codeDir}'"
+        if repo.vcs != 'git':
+            inline_fcn += f",vcs='{repo.vcs}'"
+        if replaces:
+            inline_fcn += f",replaces='{replaces}'"
+        if coderoot.pathMajor:
+            inline_fcn += f",pathmajor='{coderoot.pathMajor}'"
+        inline_fcn += ")}"
+
+        return inline_fcn, commit
+
+    def __go_handle_dependencies(self, go_mod, localfilesdir, extravalues, d):
+
+        src_uris = []
+        src_revs = []
+
+        def generate_src_rev(path, version, commithash):
+            src_rev = f"# {path}@{version} => {commithash}\n"
+            # Ups...maybe someone manipulated the source repository and the
+            # version or commit could not be resolved. This is a sign of
+            # a) the supply chain was manipulated (bad)
+            # b) the implementation for the version resolving didn't work
+            #    anymore (less bad)
+            if not commithash:
+                src_rev += f"#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
+                src_rev += f"#!!!   Could not resolve version  !!!\n"
+                src_rev += f"#!!! Possible supply chain attack !!!\n"
+                src_rev += f"#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
+            src_rev += f"SRCREV_{path.replace('/', '.')} = \"{commithash}\""
+
+            return src_rev
+
+        for require in go_mod['Require']:
+            path = require['Path']
+            version = require['Version']
+
+            inline_fcn, commithash = self.__generate_srcuri_inline_fcn(
+                path, version)
+            src_uris.append(inline_fcn)
+            src_revs.append(generate_src_rev(path, version, commithash))
+
+        if go_mod['Replace']:
+            for replacement in go_mod['Replace']:
+                oldpath = replacement['Old']['Path']
+                path = replacement['New']['Path']
+                version = replacement['New']['Version']
+
+                inline_fcn, commithash = self.__generate_srcuri_inline_fcn(
+                    path, version, oldpath)
+                src_uris.append(inline_fcn)
+                src_revs.append(generate_src_rev(path, version, commithash))
+
+        pn, _ = determine_from_url(go_mod['Module']['Path'])
+        go_mods_basename = "%s-modules.inc" % pn
+
+        go_mods_filename = os.path.join(localfilesdir, go_mods_basename)
+        with open(go_mods_filename, "w") as f:
+            # We introduce this indirection to make the tests a little easier
+            f.write("SRC_URI += \"${GO_DEPENDENCIES_SRC_URI}\"\n")
+            f.write("GO_DEPENDENCIES_SRC_URI = \"\\\n")
+            for uri in src_uris:
+                f.write("    " + uri + " \\\n")
+            f.write("\"\n\n")
+            for rev in src_revs:
+                f.write(rev + "\n")
+
+        extravalues['extrafiles'][go_mods_basename] = go_mods_filename
+
+    def __go_run_cmd(self, cmd, cwd, d):
+        return bb.process.run(cmd, env=dict(os.environ, PATH=d.getVar('PATH')),
+                              shell=True, cwd=cwd)
+
+    def __go_native_version(self, d):
+        stdout, _ = self.__go_run_cmd("go version", None, d)
+        m = re.match(r".*\sgo((\d+).(\d+).(\d+))\s([\w\/]*)", stdout)
+        major = int(m.group(2))
+        minor = int(m.group(3))
+        patch = int(m.group(4))
+
+        return major, minor, patch
+
+    def __go_mod_patch(self, srctree, localfilesdir, extravalues, d):
+
+        patchfilename = "go.mod.patch"
+        go_native_version_major, go_native_version_minor, _ = self.__go_native_version(
+            d)
+        self.__go_run_cmd("go mod tidy -go=%d.%d" %
+                          (go_native_version_major, go_native_version_minor), srctree, d)
+        stdout, _ = self.__go_run_cmd("go mod edit -json", srctree, d)
+
+        # Create patch in order to upgrade go version
+        self.__go_run_cmd("git diff go.mod > %s" % (patchfilename), srctree, d)
+        # Restore original state
+        self.__go_run_cmd("git checkout HEAD go.mod go.sum", srctree, d)
+
+        go_mod = json.loads(stdout)
+        tmpfile = os.path.join(localfilesdir, patchfilename)
+        shutil.move(os.path.join(srctree, patchfilename), tmpfile)
+
+        extravalues['extrafiles'][patchfilename] = tmpfile
+
+        return go_mod, patchfilename
+
+    def __go_mod_vendor(self, go_mod, srctree, localfilesdir, extravalues, d):
+        # Perform vendoring to retrieve the correct modules.txt
+        tmp_vendor_dir = tempfile.mkdtemp()
+
+        # -v causes to go to print modules.txt to stderr
+        _, stderr = self.__go_run_cmd(
+            "go mod vendor -v -o %s" % (tmp_vendor_dir), srctree, d)
+
+        modules_txt_basename = "modules.txt"
+        modules_txt_filename = os.path.join(localfilesdir, modules_txt_basename)
+        with open(modules_txt_filename, "w") as f:
+            f.write(stderr)
+
+        extravalues['extrafiles'][modules_txt_basename] = modules_txt_filename
+
+        licenses = []
+        lic_files_chksum = []
+        licvalues = guess_license(tmp_vendor_dir, d)
+        shutil.rmtree(tmp_vendor_dir)
+
+        if licvalues:
+            for licvalue in licvalues:
+                license = licvalue[0]
+                lics = tidy_licenses(fixup_license(license))
+                lics = [lic for lic in lics if lic not in licenses]
+                if len(lics):
+                    licenses.extend(lics)
+                lic_files_chksum.append(
+                    'file://src/${GO_IMPORT}/vendor/%s;md5=%s' % (licvalue[1], licvalue[2]))
+
+        pn, _ = determine_from_url(go_mod['Module']['Path'])
+        licenses_basename = "%s-licenses.inc" % pn
+
+        licenses_filename = os.path.join(localfilesdir, licenses_basename)
+        with open(licenses_filename, "w") as f:
+            f.write("GO_MOD_LICENSES = \"%s\"\n\n" %
+                    ' & '.join(sorted(licenses, key=str.casefold)))
+            # We introduce this indirection to make the tests a little easier
+            f.write("LIC_FILES_CHKSUM  += \"${VENDORED_LIC_FILES_CHKSUM}\"\n")
+            f.write("VENDORED_LIC_FILES_CHKSUM = \"\\\n")
+            for lic in lic_files_chksum:
+                f.write("    " + lic + " \\\n")
+            f.write("\"\n")
+
+        extravalues['extrafiles'][licenses_basename] = licenses_filename
+
+    def process(self, srctree, classes, lines_before,
+                lines_after, handled, extravalues):
+
+        if 'buildsystem' in handled:
+            return False
+
+        files = RecipeHandler.checkfiles(srctree, ['go.mod'])
+        if not files:
+            return False
+
+        d = bb.data.createCopy(tinfoil.config_data)
+        go_bindir = self.__ensure_go()
+        if not go_bindir:
+            sys.exit(14)
+
+        d.prependVar('PATH', '%s:' % go_bindir)
+        handled.append('buildsystem')
+        classes.append("go-vendor")
+
+        stdout, _ = self.__go_run_cmd("go mod edit -json", srctree, d)
+
+        go_mod = json.loads(stdout)
+        go_import = go_mod['Module']['Path']
+        go_version_match = re.match("([0-9]+).([0-9]+)", go_mod['Go'])
+        go_version_major = int(go_version_match.group(1))
+        go_version_minor = int(go_version_match.group(2))
+        src_uris = []
+
+        localfilesdir = tempfile.mkdtemp(prefix='recipetool-go-')
+        extravalues.setdefault('extrafiles', {})
+        # go.mod files with version < 1.17 may not include all indirect
+        # dependencies. Thus, we have to upgrade the go version.
+        if go_version_major == 1 and go_version_minor < 17:
+            logger.warning(
+                "go.mod files generated by Go < 1.17 might have incomplete indirect dependencies.")
+            go_mod, patchfilename = self.__go_mod_patch(srctree, localfilesdir,
+                                                        extravalues, d)
+            src_uris.append(
+                "file://%s;patchdir=src/${GO_IMPORT}" % (patchfilename))
+
+        # Check whether the module is vendored. If so, we have nothing to do.
+        # Otherwise we gather all dependencies and add them to the recipe
+        if not os.path.exists(os.path.join(srctree, "vendor")):
+
+            # Write additional $BPN-modules.inc file
+            self.__go_mod_vendor(go_mod, srctree, localfilesdir, extravalues, d)
+            lines_before.append("LICENSE += \" & ${GO_MOD_LICENSES}\"")
+            lines_before.append("require ${BPN}-licenses.inc")
+
+            self.__rewrite_src_uri(lines_before, ["file://modules.txt"])
+
+            self.__go_handle_dependencies(go_mod, localfilesdir, extravalues, d)
+            lines_before.append("require ${BPN}-modules.inc")
+
+        # Do generic license handling
+        handle_license_vars(srctree, lines_before, handled, extravalues, d)
+        self.__rewrite_lic_uri(lines_before)
+
+        lines_before.append("GO_IMPORT = \"{}\"".format(go_import))
+        lines_before.append("SRCREV_FORMAT = \"${BPN}\"")
+
+    def __update_lines_before(self, updated, newlines, lines_before):
+        if updated:
+            del lines_before[:]
+            for line in newlines:
+                # Hack to avoid newlines that edit_metadata inserts
+                if line.endswith('\n'):
+                    line = line[:-1]
+                lines_before.append(line)
+        return updated
+
+    def __rewrite_lic_uri(self, lines_before):
+
+        def varfunc(varname, origvalue, op, newlines):
+            if varname == 'LIC_FILES_CHKSUM':
+                new_licenses = []
+                licenses = origvalue.split('\\')
+                for license in licenses:
+                    license = license.strip()
+                    uri, chksum = license.split(';', 1)
+                    url = urllib.parse.urlparse(uri)
+                    new_uri = os.path.join(
+                        url.scheme + "://", "src", "${GO_IMPORT}", url.netloc + url.path) + ";" + chksum
+                    new_licenses.append(new_uri)
+
+                return new_licenses, None, -1, True
+            return origvalue, None, 0, True
+
+        updated, newlines = bb.utils.edit_metadata(
+            lines_before, ['LIC_FILES_CHKSUM'], varfunc)
+        return self.__update_lines_before(updated, newlines, lines_before)
+
+    def __rewrite_src_uri(self, lines_before, additional_uris = []):
+
+        def varfunc(varname, origvalue, op, newlines):
+            if varname == 'SRC_URI':
+                src_uri = ["git://${GO_IMPORT};destsuffix=git/src/${GO_IMPORT};nobranch=1;name=${BPN};protocol=https"]
+                src_uri.extend(additional_uris)
+                return src_uri, None, -1, True
+            return origvalue, None, 0, True
+
+        updated, newlines = bb.utils.edit_metadata(lines_before, ['SRC_URI'], varfunc)
+        return self.__update_lines_before(updated, newlines, lines_before)
+
+
+def register_recipe_handlers(handlers):
+    handlers.append((GoRecipeHandler(), 60))