diff mbox series

[1/2] fetch2: Add gomod fetcher

Message ID 20240902091323.2241464-1-christli@axis.com
State New
Headers show
Series [1/2] fetch2: Add gomod fetcher | expand

Commit Message

Christian Lindeberg Sept. 2, 2024, 9:13 a.m. UTC
From: Christian Lindeberg <christian.lindeberg@axis.com>

Add a go module fetcher for downloading module dependencies to the
module cache from a module proxy. The fetcher can be used with the
go-mod class in OE-Core.

A module dependency can be specified with:

  SRC_URI += "gomod://golang.org/x/net;version=v0.9.0;sha256sum=..."

Signed-off-by: Christian Lindeberg <christian.lindeberg@axis.com>
---
 lib/bb/fetch2/__init__.py |   4 +-
 lib/bb/fetch2/gomod.py    | 116 ++++++++++++++++++++++++++++++++++++++
 lib/bb/tests/fetch.py     |  73 ++++++++++++++++++++++++
 3 files changed, 192 insertions(+), 1 deletion(-)
 create mode 100644 lib/bb/fetch2/gomod.py

Comments

Richard Purdie Sept. 2, 2024, 10:18 a.m. UTC | #1
On Mon, 2024-09-02 at 11:13 +0200, Christian Lindeberg via lists.openembedded.org wrote:
> From: Christian Lindeberg <christian.lindeberg@axis.com>
> 
> Add a go module fetcher for downloading module dependencies to the
> module cache from a module proxy. The fetcher can be used with the
> go-mod class in OE-Core.
> 
> A module dependency can be specified with:
> 
>   SRC_URI += "gomod://golang.org/x/net;version=v0.9.0;sha256sum=..."
> 
> Signed-off-by: Christian Lindeberg <christian.lindeberg@axis.com>
> ---
>  lib/bb/fetch2/__init__.py |   4 +-
>  lib/bb/fetch2/gomod.py    | 116 ++++++++++++++++++++++++++++++++++++++
>  lib/bb/tests/fetch.py     |  73 ++++++++++++++++++++++++
>  3 files changed, 192 insertions(+), 1 deletion(-)
>  create mode 100644 lib/bb/fetch2/gomod.py

These changes look interesting and I'd be interested to hear review
from other people more used to dealing with go.

I see the code using GOMODCACHE in unpack(). In general the fetcher
tries to work off url parameters and we try not to have environment
variables unless they are truly global config. This looks to be a per
recipe local setting.

I guess this is something which any code using go is going to have to
have set into the environment anyway so perhaps it is ok but I'm not
sure I like the precedent.

I also noticed that there are two fetchers in the code, one which
subclasses git and one which subclasses wget, the difference between
them is a direct=git parameter in the url. I did wonder if these should
just be two url types (gomod:// and gomodvcs://)?

It may also be helpful to document in the code exactly what changes to
the underlying base fetcher are being made. I'm a bit worried about how
changes to the git or wget fetcher might break this code, particularly
as it isn't going to be heavily used/tested as far as I can tell.

Cheers,

Richard
Christian Lindeberg Sept. 2, 2024, 12:36 p.m. UTC | #2
On Mon, Sep 2, 2024 at 12:18 PM, Richard Purdie wrote:

> 
> On Mon, 2024-09-02 at 11:13 +0200, Christian Lindeberg via
> lists.openembedded.org wrote:
> 
>> From: Christian Lindeberg <christian.lindeberg@axis.com>
>> 
>> Add a go module fetcher for downloading module dependencies to the
>> module cache from a module proxy. The fetcher can be used with the
>> go-mod class in OE-Core.
>> 
>> A module dependency can be specified with:
>> 
>> SRC_URI += "gomod://golang.org/x/net;version=v0.9.0;sha256sum=..."
>> 
>> Signed-off-by: Christian Lindeberg <christian.lindeberg@axis.com>
>> ---
>> lib/bb/fetch2/__init__.py |   4 +-
>> lib/bb/fetch2/gomod.py    | 116 ++++++++++++++++++++++++++++++++++++++
>> lib/bb/tests/fetch.py     |  73 ++++++++++++++++++++++++
>> 3 files changed, 192 insertions(+), 1 deletion(-)
>> create mode 100644 lib/bb/fetch2/gomod.py
> 
> These changes look interesting and I'd be interested to hear review
> from other people more used to dealing with go.
> 
> I see the code using GOMODCACHE in unpack(). In general the fetcher
> tries to work off url parameters and we try not to have environment
> variables unless they are truly global config. This looks to be a per
> recipe local setting.
> 
> I guess this is something which any code using go is going to have to
> have set into the environment anyway so perhaps it is ok but I'm not
> sure I like the precedent.

I was trying to avoid having to define the location both in fetcher and
in go-mod.bbclass/recipe. But would using a directory relative rootdir
in unpack() and adapting go/mod.bbclass/recipe accordingly be preferred?

> 
> I also noticed that there are two fetchers in the code, one which
> subclasses git and one which subclasses wget, the difference between
> them is a direct=git parameter in the url. I did wonder if these should
> just be two url types (gomod:// and gomodvcs://)?

I started with a separate gomodgit:// scheme. Maybe that was clearer
after all, avoiding the extra parameter?

> 
> It may also be helpful to document in the code exactly what changes to
> the underlying base fetcher are being made. I'm a bit worried about how
> changes to the git or wget fetcher might break this code, particularly
> as it isn't going to be heavily used/tested as far as I can tell.

I was hoping the tests would help avoiding that the unpacking breaks.
Maybe verification of the resulting git or wget parameters would make
the assumptions more clear?

> 
> Cheers,
> 
> Richard

Thanks,

Christian
ChenQi Sept. 3, 2024, 6:18 a.m. UTC | #3
Can the mods be re-used (serving as some kind of mirror) from an 
existing download directory?

Regards,
Qi

On 9/2/24 17:13, Christian Lindeberg wrote:
> From: Christian Lindeberg <christian.lindeberg@axis.com>
>
> Add a go module fetcher for downloading module dependencies to the
> module cache from a module proxy. The fetcher can be used with the
> go-mod class in OE-Core.
>
> A module dependency can be specified with:
>
>    SRC_URI += "gomod://golang.org/x/net;version=v0.9.0;sha256sum=..."
>
> Signed-off-by: Christian Lindeberg <christian.lindeberg@axis.com>
> ---
>   lib/bb/fetch2/__init__.py |   4 +-
>   lib/bb/fetch2/gomod.py    | 116 ++++++++++++++++++++++++++++++++++++++
>   lib/bb/tests/fetch.py     |  73 ++++++++++++++++++++++++
>   3 files changed, 192 insertions(+), 1 deletion(-)
>   create mode 100644 lib/bb/fetch2/gomod.py
>
> diff --git a/lib/bb/fetch2/__init__.py b/lib/bb/fetch2/__init__.py
> index 5bf2c4b8c..f84ce5999 100644
> --- a/lib/bb/fetch2/__init__.py
> +++ b/lib/bb/fetch2/__init__.py
> @@ -1317,7 +1317,7 @@ class FetchData(object):
>   
>               if checksum_name in self.parm:
>                   checksum_expected = self.parm[checksum_name]
> -            elif self.type not in ["http", "https", "ftp", "ftps", "sftp", "s3", "az", "crate", "gs"]:
> +            elif self.type not in ["http", "https", "ftp", "ftps", "sftp", "s3", "az", "crate", "gs", "gomod"]:
>                   checksum_expected = None
>               else:
>                   checksum_expected = d.getVarFlag("SRC_URI", checksum_name)
> @@ -2088,6 +2088,7 @@ from . import npmsw
>   from . import az
>   from . import crate
>   from . import gcp
> +from . import gomod
>   
>   methods.append(local.Local())
>   methods.append(wget.Wget())
> @@ -2110,3 +2111,4 @@ methods.append(npmsw.NpmShrinkWrap())
>   methods.append(az.Az())
>   methods.append(crate.Crate())
>   methods.append(gcp.GCP())
> +methods.append(gomod.GoMod())
> diff --git a/lib/bb/fetch2/gomod.py b/lib/bb/fetch2/gomod.py
> new file mode 100644
> index 000000000..0675c87e5
> --- /dev/null
> +++ b/lib/bb/fetch2/gomod.py
> @@ -0,0 +1,116 @@
> +"""
> +BitBake 'Fetch' implementation for Go modules
> +
> +The gomod fetcher is used to download Go modules to the module cache from a
> +module proxy.
> +
> +Example SRC_URIs:
> +
> +SRC_URI = "gomod://golang.org/x/net;version=v0.9.0;sha256sum=..."
> +
> +Required SRC_URI parameters:
> +
> +- version
> +    The version of the module.
> +
> +Optional SRC_URI parameters:
> +
> +- mod
> +    Fetch and unpack the go.mod file only instead of the complete module.
> +    The go command may need to download go.mod files for many different modules
> +    when computing the build list, and go.mod files are much smaller than
> +    module zip files.
> +    The default is "0", set mod=1 for the go.mod file only.
> +
> +- sha256sum
> +    The checksum of the module zip file, or the go.mod file in case of fetching
> +    only the go.mod file. Alternatively, set the SRC_URI varible flag for
> +    "module@version.sha256sum".
> +
> +Related variables:
> +
> +- GOMODCACHE
> +    The location of the module cache.
> +    The variable must be exported for the go command to find the downloaded
> +    module cache.
> +
> +- GO_MOD_PROXY
> +    The module proxy used by the fetcher.
> +
> +See the Go modules reference, https://go.dev/ref/mod, for more information
> +about the module cache, module proxies and version control systems.
> +"""
> +
> +import os
> +import re
> +import shutil
> +import zipfile
> +
> +import bb
> +from bb.fetch2 import FetchError, MissingParameterError
> +from bb.fetch2.wget import Wget
> +
> +
> +def escape(path):
> +    """Escape capital letters using exclamation points."""
> +    return re.sub(r'([A-Z])', lambda m: '!' + m.group(1).lower(), path)
> +
> +
> +class GoMod(Wget):
> +    """Class to fetch Go modules from a Go module proxy via wget"""
> +
> +    def supports(self, ud, d):
> +        """Check to see if a given URL is for this fetcher."""
> +        return ud.type == 'gomod'
> +
> +    def urldata_init(self, ud, d):
> +        """Set up to download the module from the module proxy."""
> +
> +        moddir = d.getVar('GOMODCACHE')
> +        if not moddir:
> +            raise FetchError("The module cache location is not specified in the"
> +                             " GOMODCACHE environment variable.")
> +        proxy = d.getVar('GO_MOD_PROXY') or 'proxy.golang.org'
> +
> +        if 'version' not in ud.parm:
> +            raise MissingParameterError('version', ud.url)
> +
> +        module = ud.host + ud.path
> +        ud.parm['module'] = module
> +        path = escape(module + '/@v/' + ud.parm['version'])
> +        if ud.parm.get('mod', '0') == '1':
> +            path += '.mod'
> +        else:
> +            path += '.zip'
> +            ud.parm['unpack'] = '0'
> +        ud.url = bb.fetch2.encodeurl(
> +            ('https', proxy, '/' + path, None, None, None))
> +        ud.parm['downloadfilename'] = path
> +        ud.parm['subdir'] = os.path.join(moddir, 'cache', 'download',
> +                                         os.path.dirname(path))
> +        name = f"{module}@{ud.parm['version']}"
> +        if d.getVarFlag('SRC_URI', name + '.sha256sum'):
> +            ud.parm['name'] = name
> +        super().urldata_init(ud, d)
> +
> +    def unpack(self, ud, rootdir, d):
> +        """Unpack the module in the module cache."""
> +
> +        # Unpack the module zip file or go.mod file
> +        super().unpack(ud, rootdir, d)
> +
> +        if ud.localpath.endswith('.zip'):
> +            # Unpack the go.mod file from the zip file
> +            module = ud.parm['module']
> +            unpackdir = ud.parm['subdir']
> +            name = os.path.basename(ud.localpath).rsplit('.', 1)[0] + '.mod'
> +            bb.note(f"Unpacking {name} to {unpackdir}/")
> +            with zipfile.ZipFile(ud.localpath) as zf:
> +                with open(os.path.join(unpackdir, name), mode='wb') as mf:
> +                    try:
> +                        f = module + '@' + ud.parm['version'] + '/go.mod'
> +                        shutil.copyfileobj(zf.open(f), mf)
> +                    except KeyError:
> +                        # If the module does not have a go.mod file, synthesize
> +                        # one containing only a module statement.
> +                        mf.write(f'module {module}\n'.encode())
> diff --git a/lib/bb/tests/fetch.py b/lib/bb/tests/fetch.py
> index 2ef206343..652907af5 100644
> --- a/lib/bb/tests/fetch.py
> +++ b/lib/bb/tests/fetch.py
> @@ -3390,3 +3390,76 @@ class FetchPremirroronlyBrokenTarball(FetcherTest):
>               fetcher.download()
>           output = "".join(logs.output)
>           self.assertFalse(" not a git repository (or any parent up to mount point /)" in output)
> +
> +class GoModTest(FetcherTest):
> +
> +    @skipIfNoNetwork()
> +    def test_gomod_url(self):
> +        self.d.setVar('GOMODCACHE', os.path.join(self.unpackdir, 'pkg/mod'))
> +
> +        urls = ['gomod://github.com/Azure/azure-sdk-for-go/sdk/storage/azblob;version=v1.0.0;'
> +                'sha256sum=9bb69aea32f1d59711701f9562d66432c9c0374205e5009d1d1a62f03fb4fdad']
> +
> +        fetcher = bb.fetch2.Fetch(urls, self.d)
> +        ud = fetcher.ud[urls[0]]
> +        self.assertEqual(ud.url, 'https://proxy.golang.org/github.com/%21azure/azure-sdk-for-go/sdk/storage/azblob/%40v/v1.0.0.zip')
> +        self.assertNotIn('name', ud.parm)
> +
> +        fetcher.download()
> +        fetcher.unpack(self.unpackdir)
> +        downloaddir = os.path.join(self.unpackdir, 'pkg/mod/cache/download')
> +        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'github.com/!azure/azure-sdk-for-go/sdk/storage/azblob/@v/v1.0.0.zip')))
> +        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'github.com/!azure/azure-sdk-for-go/sdk/storage/azblob/@v/v1.0.0.mod')))
> +
> +    @skipIfNoNetwork()
> +    def test_gomod_url_mod_only(self):
> +        self.d.setVar('GOMODCACHE', os.path.join(self.unpackdir, 'pkg/mod'))
> +
> +        urls = ['gomod://github.com/Azure/azure-sdk-for-go/sdk/storage/azblob;version=v1.0.0;mod=1;'
> +                'sha256sum=7873b8544842329b4f385a3aa6cf82cc2bc8defb41a04fa5291c35fd5900e873']
> +
> +        fetcher = bb.fetch2.Fetch(urls, self.d)
> +        ud = fetcher.ud[urls[0]]
> +        self.assertEqual(ud.url, 'https://proxy.golang.org/github.com/%21azure/azure-sdk-for-go/sdk/storage/azblob/%40v/v1.0.0.mod')
> +        self.assertNotIn('name', ud.parm)
> +
> +        fetcher.download()
> +        fetcher.unpack(self.unpackdir)
> +        downloaddir = os.path.join(self.unpackdir, 'pkg/mod/cache/download')
> +        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'github.com/!azure/azure-sdk-for-go/sdk/storage/azblob/@v/v1.0.0.mod')))
> +
> +    @skipIfNoNetwork()
> +    def test_gomod_url_sha256sum_varflag(self):
> +        self.d.setVar('GOMODCACHE', os.path.join(self.unpackdir, 'pkg/mod'))
> +
> +        urls = ['gomod://gopkg.in/ini.v1;version=v1.67.0']
> +        self.d.setVarFlag('SRC_URI', 'gopkg.in/ini.v1@v1.67.0.sha256sum', 'bd845dfc762a87a56e5a32a07770dc83e86976db7705d7f89c5dbafdc60b06c6')
> +
> +        fetcher = bb.fetch2.Fetch(urls, self.d)
> +        ud = fetcher.ud[urls[0]]
> +        self.assertEqual(ud.url, 'https://proxy.golang.org/gopkg.in/ini.v1/%40v/v1.67.0.zip')
> +        self.assertEqual(ud.parm['name'], 'gopkg.in/ini.v1@v1.67.0')
> +
> +        fetcher.download()
> +        fetcher.unpack(self.unpackdir)
> +        downloaddir = os.path.join(self.unpackdir, 'pkg/mod/cache/download')
> +        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'gopkg.in/ini.v1/@v/v1.67.0.zip')))
> +        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'gopkg.in/ini.v1/@v/v1.67.0.mod')))
> +
> +    @skipIfNoNetwork()
> +    def test_gomod_url_no_mod_in_zip(self):
> +        self.d.setVar('GOMODCACHE', os.path.join(self.unpackdir, 'pkg/mod'))
> +
> +        urls = ['gomod://gopkg.in/ini.v1;version=v1.67.0;'
> +                'sha256sum=bd845dfc762a87a56e5a32a07770dc83e86976db7705d7f89c5dbafdc60b06c6']
> +
> +        fetcher = bb.fetch2.Fetch(urls, self.d)
> +        ud = fetcher.ud[urls[0]]
> +        self.assertEqual(ud.url, 'https://proxy.golang.org/gopkg.in/ini.v1/%40v/v1.67.0.zip')
> +        self.assertNotIn('name', ud.parm)
> +
> +        fetcher.download()
> +        fetcher.unpack(self.unpackdir)
> +        downloaddir = os.path.join(self.unpackdir, 'pkg/mod/cache/download')
> +        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'gopkg.in/ini.v1/@v/v1.67.0.zip')))
> +        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'gopkg.in/ini.v1/@v/v1.67.0.mod')))
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#16532): https://lists.openembedded.org/g/bitbake-devel/message/16532
> Mute This Topic: https://lists.openembedded.org/mt/108223851/7304865
> Group Owner: bitbake-devel+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/bitbake-devel/unsub [Qi.Chen@eng.windriver.com]
> -=-=-=-=-=-=-=-=-=-=-=-
>
Christian Lindeberg Sept. 5, 2024, 7:17 a.m. UTC | #4
On Tue, Sep 3, 2024 at 08:19 AM, Chen Qi wrote:

> 
> Can the mods be re-used (serving as some kind of mirror) from an
> existing download directory?
> 
> Regards,
> Qi

The module zip files and go.mod files are fetched with the underlying wget
fetcher (or git fecher in the git vcs repository case) so the same principles
with download cache and mirrors apply.

Thanks,
Christian
diff mbox series

Patch

diff --git a/lib/bb/fetch2/__init__.py b/lib/bb/fetch2/__init__.py
index 5bf2c4b8c..f84ce5999 100644
--- a/lib/bb/fetch2/__init__.py
+++ b/lib/bb/fetch2/__init__.py
@@ -1317,7 +1317,7 @@  class FetchData(object):
 
             if checksum_name in self.parm:
                 checksum_expected = self.parm[checksum_name]
-            elif self.type not in ["http", "https", "ftp", "ftps", "sftp", "s3", "az", "crate", "gs"]:
+            elif self.type not in ["http", "https", "ftp", "ftps", "sftp", "s3", "az", "crate", "gs", "gomod"]:
                 checksum_expected = None
             else:
                 checksum_expected = d.getVarFlag("SRC_URI", checksum_name)
@@ -2088,6 +2088,7 @@  from . import npmsw
 from . import az
 from . import crate
 from . import gcp
+from . import gomod
 
 methods.append(local.Local())
 methods.append(wget.Wget())
@@ -2110,3 +2111,4 @@  methods.append(npmsw.NpmShrinkWrap())
 methods.append(az.Az())
 methods.append(crate.Crate())
 methods.append(gcp.GCP())
+methods.append(gomod.GoMod())
diff --git a/lib/bb/fetch2/gomod.py b/lib/bb/fetch2/gomod.py
new file mode 100644
index 000000000..0675c87e5
--- /dev/null
+++ b/lib/bb/fetch2/gomod.py
@@ -0,0 +1,116 @@ 
+"""
+BitBake 'Fetch' implementation for Go modules
+
+The gomod fetcher is used to download Go modules to the module cache from a
+module proxy.
+
+Example SRC_URIs:
+
+SRC_URI = "gomod://golang.org/x/net;version=v0.9.0;sha256sum=..."
+
+Required SRC_URI parameters:
+
+- version
+    The version of the module.
+
+Optional SRC_URI parameters:
+
+- mod
+    Fetch and unpack the go.mod file only instead of the complete module.
+    The go command may need to download go.mod files for many different modules
+    when computing the build list, and go.mod files are much smaller than
+    module zip files.
+    The default is "0", set mod=1 for the go.mod file only.
+
+- sha256sum
+    The checksum of the module zip file, or the go.mod file in case of fetching
+    only the go.mod file. Alternatively, set the SRC_URI varible flag for
+    "module@version.sha256sum".
+
+Related variables:
+
+- GOMODCACHE
+    The location of the module cache.
+    The variable must be exported for the go command to find the downloaded
+    module cache.
+
+- GO_MOD_PROXY
+    The module proxy used by the fetcher.
+
+See the Go modules reference, https://go.dev/ref/mod, for more information
+about the module cache, module proxies and version control systems.
+"""
+
+import os
+import re
+import shutil
+import zipfile
+
+import bb
+from bb.fetch2 import FetchError, MissingParameterError
+from bb.fetch2.wget import Wget
+
+
+def escape(path):
+    """Escape capital letters using exclamation points."""
+    return re.sub(r'([A-Z])', lambda m: '!' + m.group(1).lower(), path)
+
+
+class GoMod(Wget):
+    """Class to fetch Go modules from a Go module proxy via wget"""
+
+    def supports(self, ud, d):
+        """Check to see if a given URL is for this fetcher."""
+        return ud.type == 'gomod'
+
+    def urldata_init(self, ud, d):
+        """Set up to download the module from the module proxy."""
+
+        moddir = d.getVar('GOMODCACHE')
+        if not moddir:
+            raise FetchError("The module cache location is not specified in the"
+                             " GOMODCACHE environment variable.")
+        proxy = d.getVar('GO_MOD_PROXY') or 'proxy.golang.org'
+
+        if 'version' not in ud.parm:
+            raise MissingParameterError('version', ud.url)
+
+        module = ud.host + ud.path
+        ud.parm['module'] = module
+        path = escape(module + '/@v/' + ud.parm['version'])
+        if ud.parm.get('mod', '0') == '1':
+            path += '.mod'
+        else:
+            path += '.zip'
+            ud.parm['unpack'] = '0'
+        ud.url = bb.fetch2.encodeurl(
+            ('https', proxy, '/' + path, None, None, None))
+        ud.parm['downloadfilename'] = path
+        ud.parm['subdir'] = os.path.join(moddir, 'cache', 'download',
+                                         os.path.dirname(path))
+        name = f"{module}@{ud.parm['version']}"
+        if d.getVarFlag('SRC_URI', name + '.sha256sum'):
+            ud.parm['name'] = name
+        super().urldata_init(ud, d)
+
+    def unpack(self, ud, rootdir, d):
+        """Unpack the module in the module cache."""
+
+        # Unpack the module zip file or go.mod file
+        super().unpack(ud, rootdir, d)
+
+        if ud.localpath.endswith('.zip'):
+            # Unpack the go.mod file from the zip file
+            module = ud.parm['module']
+            unpackdir = ud.parm['subdir']
+            name = os.path.basename(ud.localpath).rsplit('.', 1)[0] + '.mod'
+            bb.note(f"Unpacking {name} to {unpackdir}/")
+            with zipfile.ZipFile(ud.localpath) as zf:
+                with open(os.path.join(unpackdir, name), mode='wb') as mf:
+                    try:
+                        f = module + '@' + ud.parm['version'] + '/go.mod'
+                        shutil.copyfileobj(zf.open(f), mf)
+                    except KeyError:
+                        # If the module does not have a go.mod file, synthesize
+                        # one containing only a module statement.
+                        mf.write(f'module {module}\n'.encode())
diff --git a/lib/bb/tests/fetch.py b/lib/bb/tests/fetch.py
index 2ef206343..652907af5 100644
--- a/lib/bb/tests/fetch.py
+++ b/lib/bb/tests/fetch.py
@@ -3390,3 +3390,76 @@  class FetchPremirroronlyBrokenTarball(FetcherTest):
             fetcher.download()
         output = "".join(logs.output)
         self.assertFalse(" not a git repository (or any parent up to mount point /)" in output)
+
+class GoModTest(FetcherTest):
+
+    @skipIfNoNetwork()
+    def test_gomod_url(self):
+        self.d.setVar('GOMODCACHE', os.path.join(self.unpackdir, 'pkg/mod'))
+
+        urls = ['gomod://github.com/Azure/azure-sdk-for-go/sdk/storage/azblob;version=v1.0.0;'
+                'sha256sum=9bb69aea32f1d59711701f9562d66432c9c0374205e5009d1d1a62f03fb4fdad']
+
+        fetcher = bb.fetch2.Fetch(urls, self.d)
+        ud = fetcher.ud[urls[0]]
+        self.assertEqual(ud.url, 'https://proxy.golang.org/github.com/%21azure/azure-sdk-for-go/sdk/storage/azblob/%40v/v1.0.0.zip')
+        self.assertNotIn('name', ud.parm)
+
+        fetcher.download()
+        fetcher.unpack(self.unpackdir)
+        downloaddir = os.path.join(self.unpackdir, 'pkg/mod/cache/download')
+        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'github.com/!azure/azure-sdk-for-go/sdk/storage/azblob/@v/v1.0.0.zip')))
+        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'github.com/!azure/azure-sdk-for-go/sdk/storage/azblob/@v/v1.0.0.mod')))
+
+    @skipIfNoNetwork()
+    def test_gomod_url_mod_only(self):
+        self.d.setVar('GOMODCACHE', os.path.join(self.unpackdir, 'pkg/mod'))
+
+        urls = ['gomod://github.com/Azure/azure-sdk-for-go/sdk/storage/azblob;version=v1.0.0;mod=1;'
+                'sha256sum=7873b8544842329b4f385a3aa6cf82cc2bc8defb41a04fa5291c35fd5900e873']
+
+        fetcher = bb.fetch2.Fetch(urls, self.d)
+        ud = fetcher.ud[urls[0]]
+        self.assertEqual(ud.url, 'https://proxy.golang.org/github.com/%21azure/azure-sdk-for-go/sdk/storage/azblob/%40v/v1.0.0.mod')
+        self.assertNotIn('name', ud.parm)
+
+        fetcher.download()
+        fetcher.unpack(self.unpackdir)
+        downloaddir = os.path.join(self.unpackdir, 'pkg/mod/cache/download')
+        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'github.com/!azure/azure-sdk-for-go/sdk/storage/azblob/@v/v1.0.0.mod')))
+
+    @skipIfNoNetwork()
+    def test_gomod_url_sha256sum_varflag(self):
+        self.d.setVar('GOMODCACHE', os.path.join(self.unpackdir, 'pkg/mod'))
+
+        urls = ['gomod://gopkg.in/ini.v1;version=v1.67.0']
+        self.d.setVarFlag('SRC_URI', 'gopkg.in/ini.v1@v1.67.0.sha256sum', 'bd845dfc762a87a56e5a32a07770dc83e86976db7705d7f89c5dbafdc60b06c6')
+
+        fetcher = bb.fetch2.Fetch(urls, self.d)
+        ud = fetcher.ud[urls[0]]
+        self.assertEqual(ud.url, 'https://proxy.golang.org/gopkg.in/ini.v1/%40v/v1.67.0.zip')
+        self.assertEqual(ud.parm['name'], 'gopkg.in/ini.v1@v1.67.0')
+
+        fetcher.download()
+        fetcher.unpack(self.unpackdir)
+        downloaddir = os.path.join(self.unpackdir, 'pkg/mod/cache/download')
+        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'gopkg.in/ini.v1/@v/v1.67.0.zip')))
+        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'gopkg.in/ini.v1/@v/v1.67.0.mod')))
+
+    @skipIfNoNetwork()
+    def test_gomod_url_no_mod_in_zip(self):
+        self.d.setVar('GOMODCACHE', os.path.join(self.unpackdir, 'pkg/mod'))
+
+        urls = ['gomod://gopkg.in/ini.v1;version=v1.67.0;'
+                'sha256sum=bd845dfc762a87a56e5a32a07770dc83e86976db7705d7f89c5dbafdc60b06c6']
+
+        fetcher = bb.fetch2.Fetch(urls, self.d)
+        ud = fetcher.ud[urls[0]]
+        self.assertEqual(ud.url, 'https://proxy.golang.org/gopkg.in/ini.v1/%40v/v1.67.0.zip')
+        self.assertNotIn('name', ud.parm)
+
+        fetcher.download()
+        fetcher.unpack(self.unpackdir)
+        downloaddir = os.path.join(self.unpackdir, 'pkg/mod/cache/download')
+        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'gopkg.in/ini.v1/@v/v1.67.0.zip')))
+        self.assertTrue(os.path.exists(os.path.join(downloaddir, 'gopkg.in/ini.v1/@v/v1.67.0.mod')))