Migrate to Go modules while keep on using distribution packages - go

I have a GOPATH based project that I currently build like this on Fedora:
sudo dnf install golang-etcd-bbolt-devel golang-x-sys-devel golang-x-text-devel
GOPATH=$HOME/go:/usr/share/gocode go build
My project (gonzofilter) implements a command line utility and thus source files are located in the main package (i.e. they have a package main declaration).
With Fedora 34 and beyond it seems that Go removed support for building GOPATH style projects and one really has to use Go modules:
go build
go: cannot find main module; see 'go help modules'
That Go blog post kind of covers my case (-> 'Without a dependency manager'), but it doesn't explicitly mention how to deal with main package projects or with distribution provided dependencies.
So, how do I migrate such a project?
How do I have to tell Go/go mod tidy to look for my dependencies under /usr/share/gocode?
Edit: To be precise: Fedora 34 comes with Go 1.16 which 'just' changed the GO111MODULE default from auto to on. Thus, one still can restore the old behavior by setting GO111MODULE=auto.
However, Golang developers already announced that they want to drop support for GOPATH style projects in Go 1.17:
We plan to drop support for GOPATH mode in Go 1.17. In other words, Go 1.17 will ignore GO111MODULE. If you have projects that do not build in module-aware mode, now is the time to migrate.
Update 2023-02-19: As of Go 1.19.5 (Fedora 37), GO111MODULE=off still works for building GOPATH style (i.e. non-modularized) projects. Example:
export GOPATH=$HOME/go:/usr/share/gocode GOPROXY=off GO111MODULE=off
go build helloworld.go
Apparently, the Go team adjusted their GO11MODULE deprecation plan, without targeting any new future release for removal, yet.

You can explicity define using replace keyword in your generated mod file to refer to your local modules.
replace packagename => /usr/share/gcode

Ideally, distributions should package installed dependencies in a layout that is compatible with the GOPROXY protocol. Then you would be able to set GOPROXY appropriately and run go mod tidy to use the installed dependencies.
However, to my knowledge, no distributions actually provide a GOPROXY tree at this point. You may need a workaround.
The next best alternative is to wire up the replacements yourself using replace directives. go mod tidy already knows to prefer the versions found in replace directives over other versions, so it should suffice to do something like:
go mod init github.com/gsauthof/gonzofilter
go mod edit -replace go.etcd.io/bbolt=/usr/share/gocode/src/go.etcd.io/bbolt
go mod edit -replace golang.org/x/sys=/usr/share/gocode/src/golang.org/x/sys
go mod edit -replace golang.org/x/text=/usr/share/gocode/src/golang.org/x/text
go mod tidy
However, note that the replace directive requires the target to include an explicit go.mod file, so this will only work if either your dependencies already have explicit go.mod files or your distro packager has added them as a patch to support this use-case.
Note that with those replace directives in place, your build would not be reproducible or repeatable over time: if you change the distro-installed versions of your dependencies, then the meaning of go build will silently change to use those different (and possibly incompatible!) versions as well.
So I would recommend that you not do that, and instead use go get and/or go mod tidy to fetch the specific module versions you want from upstream or from a public module proxy (such as proxy.golang.org).
If you are concerned about upstream tampering, note that the Go project's official binary distribution of the go command — and the go command built from pristine source — by default automatically verifies checksums for downloaded modules against an auditable public database at https://sum.golang.org; however, the Fedora distribution of the go command disables use of the checksum database by default.

One way to migrate such a GOPATH project to a Go modules aware one is the following:
First, manually create the go.mod with the direct dependencies:
module github.com/gsauthof/gonzofilter
go 1.15
require (
go.etcd.io/bbolt v1.3.5
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7
golang.org/x/text v0.3.5
)
I got the minimal versions from what's currently on my system:
$ rpm -q golang-etcd-bbolt-devel golang-x-sys-devel golang-x-text-devel golang-bin
golang-etcd-bbolt-devel-1.3.5-2.fc33.noarch
golang-x-sys-devel-0-0.39.20210123git9b0068b.fc33.noarch
golang-x-text-devel-0.3.5-1.fc33.noarch
golang-bin-1.15.8-1.fc33.x86_64
The problem now is that one can't simply tell the module-aware Go tools to look for those modules under the filesystem path /usr/share/gocode/src where they are all located.
There is the replace directive one can add to our go.mod file to set the lookup path for single dependencies, e.g. like this:
replace (
go.etcd.io/bbolt => /usr/share/gocode/src/go.etcd.io/bbolt
golang.org/x/sys => /usr/share/gocode/src/golang.org/x/sys
golang.org/x/text => /usr/share/gocode/src/golang.org/x/text
)
However, a go build still doesn't use indirect dependencies from /usr/share/gocode, e.g. the ones that are provided by the packages golang-x-text-devel depends on. In this example, go build does find /usr/share/gocode/src/golang.org/x/text/go.mod but that file doesn't include any replace directive.
Thus, this fails:
$ GOPROXY=off go build -mod=readonly
go: golang.org/x/text#v0.3.6 requires
golang.org/x/tools#v0.0.0-20180917221912-90fa682c2a6e: module lookup disabled
by GOPROXY=off
To resolve this we have to add replace directive lines also for all indirect dependencies.
Doing this manually is tedious and error prone, of course.
Thus, we can automate this with a small shell one-liner:
We start with this go.mod file:
module github.com/gsauthof/gonzofilter
go 1.15
require (
go.etcd.io/bbolt v1.3.5
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7
golang.org/x/text v0.3.6
)
replace (
//replace-this
)
The following fixpoint iteration then adds dependencies until the build succeeds:
while true; do
GOPROXY=off go build -mod readonly 2> t
if [ $? -eq 1 ] && grep 'module lookup disabled\|missing go.sum entry' t >/dev/null; then
x=$(grep 'disabled\|missing' t | tr ' \t' '\n' | grep '#' | head -n1 | cut -d'#' -f1 )
echo "Adding $x"
sed -i "s#^\(//replace-this\)#\t$x => /usr/share/gocode/src/$x\n\1#" go.mod
continue
fi
break
done
Which results in the following require directive for this example project:
replace (
go.etcd.io/bbolt => /usr/share/gocode/src/go.etcd.io/bbolt
golang.org/x/sys => /usr/share/gocode/src/golang.org/x/sys
golang.org/x/text => /usr/share/gocode/src/golang.org/x/text
golang.org/x/tools => /usr/share/gocode/src/golang.org/x/tools
github.com/yuin/goldmark => /usr/share/gocode/src/github.com/yuin/goldmark
golang.org/x/mod => /usr/share/gocode/src/golang.org/x/mod
golang.org/x/net => /usr/share/gocode/src/golang.org/x/net
golang.org/x/sync => /usr/share/gocode/src/golang.org/x/sync
golang.org/x/xerrors => /usr/share/gocode/src/golang.org/x/xerrors
golang.org/x/crypto => /usr/share/gocode/src/golang.org/x/crypto
golang.org/x/term => /usr/share/gocode/src/golang.org/x/term
//replace-this
)

Another way to point Go to the system-wide available dependencies is to exploit Go's module vendoring support: That means by symbolically linking the module file paths, creating a minimal vendor/modules.txt and compiling with -mod vendor.
So with -mod vendor Go build doesn't try to download any required modules, instead it looks them up in vendor/ directory.
One might be tempted to just create a symbolic link that points from vendor/ to /usr/share/gocode/src (where all the distributions golang-...-devel packages are installed), but this doesn't work because -mod vendor also requires yet another modules description file, i.e. vendor/modules.txt.
So, besides some more specific symlinks, we have to create a proper modules.txt when using the vendoring feature:
First, to create all the sublevel symlinks:
awk -vvd=wendor 'BEGIN {
PROCINFO["sorted_in"]="#ind_str_asc";
system("mkdir -p "vd)
}
func push_mod(pkg) {
if (!seen[pkg]) {
ARGV[ARGC++]="/usr/share/gocode/src/"pkg"/go.mod";
seen[pkg]=1;
}
sub("/.+$", "", pkg);
xs[pkg]=1;
}
/^require[^(]+$/ {
push_mod($2);
next
}
/^require/ {
inb=1;
next
}
/^\)/ {
inb=0;
next
}
inb {
push_mod($1);
}
END {
for (i in xs) {
print i;
system("ln -sfn /usr/share/gocode/src/"i" "vd"/"i)
}
}' go.mod
Then, to create the vendor/modules.txt:
awk -vvd=wendor 'BEGIN {
fn=vd"/modules.txt"
}
/^require/ {
inb=1;
next
}
/^\)/ {
inb=0;
next
}
inb {
printf("# %s %s\n## explicit\n%s\n", $1, $2, $1) > fn
}' go.mod
After those commands, the vendor directory looks like this for our example project:
$ ls -l vendor
total 16
lrwxrwxrwx. 1 juser juser 32 2021-04-25 11:12 github.com -> /usr/share/gocode/src/github.com
lrwxrwxrwx. 1 juser juser 32 2021-04-25 11:12 go.etcd.io -> /usr/share/gocode/src/go.etcd.io
lrwxrwxrwx. 1 juser juser 32 2021-04-25 11:12 golang.org -> /usr/share/gocode/src/golang.org
-rw-r--r--. 1 juser juser 195 2021-04-25 11:13 modules.txt
$ cat vendor/modules.txt
# go.etcd.io/bbolt v1.3.5
## explicit
go.etcd.io/bbolt
# golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7
## explicit
golang.org/x/sys
# golang.org/x/text v0.3.5
## explicit
golang.org/x/text
Whereas go.mod is quite minimal, i.e. it doesn't contain any replace directive:
module github.com/gsauthof/gonzofilter
go 1.15
require (
go.etcd.io/bbolt v1.3.5
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7
golang.org/x/text v0.3.5
)
And this is sufficient to let
GOPROXY=off go build -mod vendor
succeed.
Note that indirect dependencies such as golang.org/x/tools aren't listed in the modules.txt, however, they are still available via vendor/ because we transitively traversed all go.mod files starting with the required modules when creating symlinks to the top level directories.
The discrete charme of this scheme is that one doesn't have to mess with replace directives in the go.mod file. That means go.mod can be kept quite generic, one basically just have to add the -mod vendor switch to the build command.

Related

Content of go.sum and modules really used by a go application

I'm trying to compare the behavior of go mod tidy (and the resulting content of go.sum) to the output of go list -m all.
Reading the docs, I understand go.sum contains the whole list of dependent modules declared in go.mod and in dependencies' go.mod files, go list -m all shows the modules really loaded during the execution.
As an example, an application including logrus and prometheus like this:
go.mod
module mytest
go 1.14
require (
github.com/prometheus/common v0.4.0
github.com/sirupsen/logrus v1.8.1
)
main.go
package main
import "github.com/sirupsen/logrus"
import "github.com/prometheus/common/version"
func main() {
logrus.Info("Hello World")
logrus.Infof("Prometheus info: %v", version.Info())
}
After go mod tidy, go.sum shows both logrus v1.8.1, requested by the go.mod, and 1.2.0, dependency of prometheus v0.4.0; go list -m all shows only v1.8.1.
go.sum
[...]
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
[...]
output of go list
[...]
github.com/sirupsen/logrus v1.8.1
[...]
Is it correct to say the modules really used by the application are listed by go list -m all?
The underlying problem is that a static code analysis detects insecure module versions listed in go.sum, but actually these versions don't show up in go list -m all, hence they shouldn't be really used by the application, but only downloaded during build phase to select the proper minimal version.
Some reference:
https://go.dev/ref/mod#go-mod-tidy
go mod tidy acts as if all build tags are enabled, so it will consider
platform-specific source files and files that require custom build
tags, even if those source files wouldn’t normally be built.
https://go.dev/ref/mod#go-sum-files
The go.sum file may contain hashes for multiple versions of a module.
The go command may need to load go.mod files from multiple versions of
a dependency in order to perform minimal version selection. go.sum may
also contain hashes for module versions that aren’t needed anymore
(for example, after an upgrade).
https://github.com/golang/go/wiki/Modules#is-gosum-a-lock-file-why-does-gosum-include-information-for-module-versions-i-am-no-longer-using
[...]In addition, your module's go.sum records checksums for all
direct and indirect dependencies used in a build (and hence your
go.sum will frequently have more modules listed than your go.mod).
https://github.com/golang/go/wiki/Modules#version-selection
The minimal version selection algorithm is used to select the versions
of all modules used in a build. For each module in a build, the
version selected by minimal version selection is always the
semantically highest of the versions explicitly listed by a require
directive in the main module or one of its dependencies.
As an example, if your module depends on module A which has a
require D v1.0.0, and your module also depends on module B which
has a require D v1.1.1, then minimal version selection would choose
v1.1.1 of D to include in the build (given it is the highest listed
require version). [...] To see a list of the selected module versions
(including indirect dependencies), use go list -m all.
Yes, it correct to say the modules really "used" by the application are listed by go list -m all (as per documentation you provided the link of). By "used", it means the package selected at build time for the compilation of the go code of your application.
We had a similar issue with a static analysis tool and we had to change the configuration to use the output of go list -m all (dumped in a file) instead of go.sum.

force a transitive dependency version in golang

I have a question about dependencies in golang.
My application defines a go.mod like this:
module my.host.com/myapp
require (
ext1.com/module1 v0.0.1
)
go 1.14
The dependency relationship is:
ext1.com/module1 v0.0.1 depends on ext3.com/module3 v0.0.3
A security scan detects ext3.com/module3 v0.0.3 is insecure and must be updated to v0.0.4.
Is there a way to "force" myapp to get only module3 v0.0.4, overriding the directives defined in module1 v0.0.1 go.mod?
Let's say ext1.com/module1 v0.0.1 is already at the latest version, so upgrading it doesn't work.
Would "replace" work?
module my.host.com/myapp
require (
ext1.com/module1 v0.0.1
)
replace ext3.com/module3 v0.0.3 => ext3.com/module3 v0.0.4
go 1.14
Thanks in advance!
Run go get -u ext3.com/module3#v0.0.4.
This upgrades the module to at least the v0.0.4
Given the dependency main -> B -> C, when main requires a higher version of C than that required by B, the higher version is selected, with // indirect.
See this https://go.dev/ref/mod#go-mod-file-require
If the go directive specifies go 1.16 or lower, the go command adds an indirect requirement when the selected version of a module is higher than what is already implied (transitively) by the main module’s other dependencies. That may occur because of an explicit upgrade (go get -u ./...)
I quote this part because your go.mod has go 1.14

How can I force go mod to accept a module that declares its path being different from its go.mod?

When I run go mod tidy, it breaks because a package imported by my project imports another package using path github.com/coreos/bbolt, but when it fetches the package from this path its go.mod says its path is go.etcd.io/bbolt.
The problem is that both the importing package and the imported package are 3rd party packages. I know I could edit the go module cache to fix it, but it would be a real hell fixing it when new versions of these packages become available.
Partial echoed messages are shown below:
github.com/coreos/etcd/client tested by
github.com/coreos/etcd/client.test imports
github.com/coreos/etcd/integration imports
github.com/coreos/etcd/etcdserver imports
github.com/coreos/etcd/mvcc/backend imports
github.com/coreos/bbolt: github.com/coreos/bbolt#v1.3.5: parsing go.mod:
module declares its path as: go.etcd.io/bbolt
but was required as: github.com/coreos/bbolt
So, how can I fix or work around this situation?
You can fix this solution by using the replace directive
Simply add:
replace github.com/coreos/bbolt v1.3.5 => go.etcd.io/bbolt v1.3.5
at the end of your go.mod file
The mismatched path implies that your dependency (github.com/coreos/etcd/mvcc/backend) is written against an old version of the bbolt repository — one that predates commit e65d4d.
I notice that the current go.mod file in the github.com/etcd-io/etcd repo specifies its module path as go.etcd.io/etcd/v3.
So the most robust fix for you is probably to update to that path, which you can do by changing your import statements to refer to the new canonical import path and running go mod tidy to update your dependencies accordingly:
sed -i s,github.com/coreos/etcd,go.etcd.io/etcd/v3,g $(find . -name '*.go')
go mod tidy
Barring that, you could explicitly choose a version of github.com/coreos/bbolt that matches the older import path. I notice that the highest version for that module listed at https://beta.pkg.go.dev/github.com/etcd-io/bbolt?tab=versions is v1.3.3, and v1.3.4 does seem to add a go.mod file with the updated path. So as a fallback, you could try:
go get -d github.com/coreos/bbolt#v1.3.3
The downside to that approach is that v1.3.3 is the end of the line: you wouldn't be able to pull in bug-fixes after that point, because those fixes are all at the go.etcd.io path.
You could also use the replace directive with the command line, as example:
go mod edit -replace github.com/pselle/bar=/Users/pselle/Projects/bar
More info in this article

Go modules replace with a specific version of a local module

I have a go.mod file that looks like this:
module someName
go 1.13
require (
.
.
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4
localpackage v0.0.0
)
replace localpackage => ../localpackage
This works just as expected! What I can't figure out is how can I add a certain version or commit hash to the replace directive!
For example:
replace localpackage => ../localpackage v1.0.0
or
replace localpackage => ../localpackage v0.0.0-20190731182444-35453ccff3d6
Doing this results in an error:
replacement module directory path "../localpackage" cannot have version
The error is quite clear that I shouldn't add a version to a local replace. I checked the wiki but I couldn't find an answer!
The question:
Is it possible to add this kind of replacement and how? What am I missing here?
As the error says: you cannot specify a version when the replace directive is pointed at a local folder. There is no guarantee and it is not a requirement that the replacement folder contains files of a versioning system, it is perfectly valid to just have "snaphots" of the Go sources. So in many cases it would have no meaning to specify a version.
However, if your local folder is a clone of a git repository for example, you may simply switch that to your intended version. E.g. execute a git checkout v1.0.0 in that local folder to switch to that version, and that version will be used.

Using the replace verb in go modules for labix.org mgo

Using go modules, I would like to replace labix.org/v2/mgo with github.com/globalsign/mgo. The http://labix.org/mgo repository is unmaintained and has been forked to https://github.com/globalsign/mgo
my code is stored outside $GOPATH in the directory ~/git/foo
I'm using go1.11
other go modules are working (for example go list -m all lists other modules, the files go.mod and go.sum are updating automatically. See the full file below)
I've tried the following in the go.mod file:
replace labix.org/v2/mgo => github.com/globalsign/mgo v0.0.0-20181015145952-eeefdecb41b842af6dc652aaea4026e8403e62df
Running go build gives the following error:
build github.com/foo/bar: cannot find module for path labix.org/v2/mgo
The documentation in go help modules discusses Pseudo-Versions eg v0.0.0-yyy.., which I'm trying to use because the tags on https://github.com/globalsign/mgo are of the form r2018.06.15 and not v1.2.3 (semantic versioning).
In addition go help modules says:
Pseudo-versions never need to be typed by hand: the go command will accept
the plain commit hash and translate it into a pseudo-version (or a tagged
version if available) automatically. This conversion is an example of a
module query.
However I can't work out the command for generating a pseudo-version when I'm in the cloned github.com/globalsign/mgo (located at $GOPATH/src/github.com/globalsign/mgo). Hence the pseudo-version I've manually generated may be wrong.
The full go.mod file looks like:
module github.com/foo/bar
replace labix.org/v2/mgo => github.com/globalsign/mgo v0.0.0-20181015145952-eeefdecb41b842af6dc652aaea4026e8403e62df
require (
github.com/DATA-DOG/godog v0.7.8
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 // indirect
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect
...
)
Try the go edit -replace command, without modifying the line of the original package.
This way you don't need to know the exact pseudo version beforehand, just the commit hash id.
So in your case, don't manually modify go.mod require section of "labix.org/v2/mgo", and run this command to append your commit "eeefdec" of your fork, on top of labix version:
$ GO111MODULE="on" go mod edit -replace=labix.org/v2/mgo=github.com/globalsign/mgo#eeefdec
After the first time you build or test your module, GO will try to pull the new commit, and then will generate the "replace" line with the correct pseudo version for you. So then you'll see on the bottom of go.mod:
replace labix.org/v2/mgo => github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
When using the replace directive, leave the pseudo-version out.
It's also stated here, which points to an open issue.
Probably off-topic, but I've mostly used replace when I wanted to use a local version of some dependency. Why not import the forked lib which you'd like to use (instead of the original non-maintained one) and have mod resolve it properly?
The source being replaced (in this case labix.org/v2/mgo) also needs to be added to the require list with a version of v0.0.0 (even though it won't be downloaded). In the replace the source doesn't need a version but the target does.
However I haven't worked out how to auto-generate the pseudo version for the target (github.com/globalsign/mgo in this case), so I still needed to manually generate it.
Here's a better go.mod file:
1 module foo.bar/qux
2
3 replace labix.org/v2/mgo => github.com/globalsign/mgo v0.0.0-20181015145952-eeefdecb41b842af6dc652aaea4026e8403e62df
4
5 require (
6 github.com/DATA-DOG/godog v0.7.8
7 github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
<snip>
21 golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b // indirect
22 labix.org/v2/mgo v0.0.0
23 )
Notice the require of labix.org on line 22; go mod tidy accepts this.
However I now come up against a different issue, the use of internal packages (referred to in issues like this: https://github.com/golang/go/issues/23970). I still haven't solved this new problem.
% go build main.go
/home/sonia/go/pkg/mod/github.com/globalsign/mgo#v0.0.0-20181015145952-eeefdecb41b842af6dc652aaea4026e8403e62df/auth.go:38:2:
use of internal package github.com/globalsign/mgo/internal/scram not allowed

Resources