golang/go

cmd/go: go install cmd@version errors out when module with main package has replace directive

james-lawrence opened this issue Β· 45 comments

What version of Go are you using (go version)?

go version
go version go1.16 linux/amd64

Does this issue reproduce with the latest release?

yes

What did you do?

cd /tmp; go install bitbucket.org/jatone/genieql@latest

What did you expect to see?

go install finishing successfully

What did you see instead?

go install bitbucket.org/jatone/genieql@latest: bitbucket.org/jatone/genieql@v0.0.0-20210306180040-be9c7b630c18
	The go.mod file for the module providing named packages contains one or
	more replace directives. It must not contain directives that would cause
	it to be interpreted differently than if it were the main module.

given that genieql is the main module this error appears to be erroneous.

other attempts I made all of which i would have expected to work properly (and use to):

cd /tmp; go install bitbucket.org/jatone/genieql/...@latest
go install bitbucket.org/jatone/genieql/...@latest: bitbucket.org/jatone/genieql@v0.0.0-20210306180040-be9c7b630c18
	The go.mod file for the module providing named packages contains one or
	more replace directives. It must not contain directives that would cause
	it to be interpreted differently than if it were the main module.
cd /tmp; go install bitbucket.org/jatone/genieql/cmd/...@latest
go install bitbucket.org/jatone/genieql/cmd/...@latest: bitbucket.org/jatone/genieql@v0.0.0-20210306180040-be9c7b630c18
	The go.mod file for the module providing named packages contains one or
	more replace directives. It must not contain directives that would cause
	it to be interpreted differently than if it were the main module.
cd /tmp; go install bitbucket.org/jatone/genieql/cmd/genieql@latest
go install bitbucket.org/jatone/genieql/cmd/genieql@latest: bitbucket.org/jatone/genieql@v0.0.0-20210306180040-be9c7b630c18
	The go.mod file for the module providing named packages contains one or
	more replace directives. It must not contain directives that would cause
	it to be interpreted differently than if it were the main module.

L28 contains a replace directive

replace github.com/containous/yaegi => github.com/james-lawrence/yaegi v0.8.8-modules-enh

Which is not valid for go install

No module is considered the "main" module. If the module containing packages named on the command line has a go.mod file, it must not contain directives (replace and exclude) that would cause it to be interpreted
differently than if it were the main module

Closing as working as intended

yes obviously line 28 contains the directive. see the title. and then see this thread where I was told to open this issue.

that would cause it to be interpreted differently than if it were the main module

is the relevant line of that statement here. the fact is this directive wouldn't cause it to be interpreted differently than if it was the main module. and to any outside viewer, there is a main module, its the one being installed.

reopening, I would recommend rephrasing the issue in terms of a proposal to relax the behaviour rather than as a bug report

@seankhliao I'm just not sure if it is a bug or a proposal, the discussion around go installs (new) behavior was long, varied, and confused. =) this particular problem was pointed out during that discussion and seemingly ignored/disregarded/left open to be revisited. so here we are.

@james-lawrence

this particular problem was pointed out during that discussion and seemingly ignored/disregarded/left open to be revisited.

#40276 was indeed discussed at length. Indeed, over the years even prior to that specific proposal, this issue has been discussed in various forms, many times. To say that the question of replace directives was ignored/disregarded is incorrect. Indeed I specifically linked to a quote that pointed out the option for that particular point to be revisited. It suffices therefore to open an issue, as suggested, to start that discussion.

Deferring to @jayconrod on how best to take the discussion forward.

I meant it was at one of those 3 at various points in that (and the related) thread. I wasn't calling out any particular stage, sorry for that confusion.

I think there may be confusion around the concepts of a main module and the package main.
The main module is normally the one in which the go command was invoked (no matter what packages are being built or installed). Modules cannot be installed, only packages. When building in package@version mode there is no main module.

In the case in this issue genieql is the package main, and the way in which it would be built changes if the module of which it is a part is the main module because of the replace statement.
For example, if you did go install bitbucket.org/jatone/genieql in another module that had bitbucket.org/jatone as a dependency it would not obey the replace directive. If you checkout bitbucket.org/jatone and do go install ./genieql then it will obey the replace directives and produce a different result. It would also obey the versions specified in the main module, which would change the results even further.

Also note that it is not something that used to work, it is a brand new feature that limits the number of cases in which it is enabled.

Maybe we should change the terminology for main module to a different word?

Also note that it is not something that used to work

while technically true go get -u did work because it respected the vendor directory: go114 get -u bitbucket.org/jatone/genieql-go114/cmd/... see related issue #44841 and #40053 + the google groups thread in that issue.

cat ~/go/src/bitbucket.org/jatone/genieql-go114/vendor/github.com/containous/yaegi/interp/src.go | grep -i 'func pkgDir(ctx \*build.Context'

returns the patched function from the replace + vendor workflow, and if you started with a clean environment there would be no other sources of the library in question.

now did it work if trying to use a package as a dependency? no, and that was fine because it wasn't about using it as a library. many go programs are never intended to be used that way.

now the rest of your argument is attempting to explain away for current limitations after the fact that go get / go install commands were fundamentally changed to support go modules while simultaneously ignoring vendor workflows of the tools and making that argument with a distinction that never existed until the tools were changed.

now all that being said: installing a binary is different from managing dependencies. I believe this is an accepted fact and a reason go get was changed to be about managing dependencies and go install is being shift towards building and installing binaries. this fundamentally is a good change and I'm wholeheartedly behind it.

I think the question we need to be asking is if the distinction between main package of the main module during installation matter? I'm going to assert it does not and that this error is erroneous, but during dependency management where someone tried to add a dependency that had a replace directive this error would be 100% correct though I'd start out with a warning in that case vs a hard error.

Ah I believe I have a good explanation about why this should be supported.

the replace directive is fundamentally about compilation, its meant to inform the build system how to patch dependencies. this is true both during development where you may need to patch them while debugging a problem which is when you compile your program. and during installation of a main module (previously the main package) onto a system.

so during compilation there is no distinction between the main package and the main module, however, during dependency management replace directives have always been ignored and any software that was relying on a dependency where there was a replace directive was broken (conceptually if not in practice). and this is where we should be erroring out (i.e. in go get in this new world) when we see a replace directive. not during compilation (go install).

Closing this issue since it's working as described in #40276.

That being said, at some point in the future when we have more information, it will be reasonable to open a proposal to either ignore or apply replace and exclude directives. That proposal should be based on the discussion in #40276 (and previously #30515), it must include data backing up whichever position is supported, and it must consider tradeoffs for the other side.

I know this is a lot to ask (those issues are very long), but the main point of disagreement was what to do with replace directives, and we debated that for a year and a half. The outcome was a compromise: replace directives are not allowed for now, and we have the option of changing that in the future without making an incompatible change.

To restart that discussion, we need new information: how is the new go install cmd@version form being used, and would replace directives be helpful or harmful?

ncw commented

@jayconrod wrote:

To restart that discussion, we need new information: how is the new go install cmd@version form being used, and would replace directives be helpful or harmful?

As a go user, I reasonably expect go install cmd@version to just work and install the binary cmd using the go.mod the authors of that package spent time getting exactly right.

Instead I get an extremely cryptic message like this

go install: github.com/rclone/rclone@latest (in github.com/rclone/rclone@v1.57.0):
	The go.mod file for the module providing named packages contains one or
	more replace directives. It must not contain directives that would cause
	it to be interpreted differently than if it were the main module.

As a user of said cmd, I have no idea how to fix that problem.

From the instructions for go install cmd@version: https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies need amending

No module is considered the "main" module. If the module containing packages named on the command line has a go.mod file, it must not contain directives (replace and exclude) that would cause it to be interpreted differently than if it were the main module. The module must not require a higher version of itself.

Which says the same thing as the error message, more or less.

So it is consistent, but not helpful in my opinion!

It is very hard having a large open source project without a few soft forks while you are waiting for upstreams to take your patches.

I first noticed this problem with github's gh tool, and I think you'll find that there are lot more high profile Go binaries out there with this problem.

$ go install github.com/cli/cli/cmd/gh@latest
go install: github.com/cli/cli/cmd/gh@latest (in github.com/cli/cli@v1.14.0):
	The go.mod file for the module providing named packages contains one or
	more replace directives. It must not contain directives that would cause
	it to be interpreted differently than if it were the main module.
mitar commented

I read the proposal in #40276 and it concludes with:

The modules I'm most concerned about are those that use replace as a soft fork while submitting a bug fix to an upstream module; other problems have other solutions that I don't think we need to design for here. Modules using soft fork replacements are about 4% of the the modules with go.mod files I sampled (165 / 4519). This is a small enough set that I think we should move forward with the proposal above.

I am sad to learn that 4% is small for some, but it is still a big one when you are one of those inside 4%.

I think those soft forks are really important use case to support. We have this situation just now, we have a tool which depends on 3rd party library. That library have a data-loss bug users of our tool might encounter so we made a PR upstream with a fix and use replace to fix our tool. But now we cannot really release our tool but have to wait for upstream to merge it. In meantime, all our users might be loosing data. So now we have to wait for upstream to merge it.

You can imagine similar scenario for a security issue, too. So sometimes a soft fork is useful to get an important fix out soon. We can build a binary and release it so I am not sure why users cannot do go install ... @version to get exactly the same binary, just compiled for them.

@mitar Similarly I have a lot of soft-forks which I need replace directives for. The users can't install with "go install" because of the limitation on replace directives, but it would make way more sense if Go install just behaved as if I had run "go install" with cwd set to the root of the module containing the referenced package. Issue: go install -v github.com/aperturerobotics/bifrost/cmd/bifrost@master - fails w/ this error.

I hit this too.

$ go get github.com/estesp/manifest-tool/v2/cmd/manifest-tool@v2.0.0
go get: installing executables with 'go get' in module mode is deprecated.
	Use 'go install pkg@version' instead.
	For more information, see https://golang.org/doc/go-get-install-deprecation
	or run 'go help get' or 'go help install'.

$ go install github.com/estesp/manifest-tool/v2/cmd/manifest-tool@v2.0.0
go install: github.com/estesp/manifest-tool/v2/cmd/manifest-tool@v2.0.0 (in github.com/estesp/manifest-tool/v2@v2.0.0):
	The go.mod file for the module providing named packages contains one or
	more replace directives. It must not contain directives that would cause
	it to be interpreted differently than if it were the main module.

Do I misinterpret that there's just no recourse here?

@jayconrod or other maintainers: given that this issue has a lot of traffic & there will be a lot of projects with replace directives:

Clearly people would like to be able to "go install" things with replace directives.

Ideally it would just respect the directives of the module containing the desired build target.

Given that both this issue & the ones linked to in the closing message are all closed, there's no path forward to this right now (or is there?) - could we re-open this at least?

Pinging this again, in case there's a canonical answer and we're all just missing it?

@thockin I think someone will need to make a new issue?

We have soft forks of tools due to various reasons and now we are blocked from upgrading from go 1.16 b/c of this issue. Given that Go 1.16 isn't being supported forever we'll soon be stuck with outdated software or rewrite a lot of module imports to make hard forks...

The same thing. We can't install 3rd party tools with go 18 any more.

I wish I could install it too.

I would like to be able to install it with go install as I would like many people to test it before I soft fork it and submit a pull request to the developer.

I wish I could install it too.

I would like to be able to install it with go install as I would like many people to test it before I soft fork it and submit a pull request to the developer.

oh yes, this looks like a mandarory use-case

@jayconrod this seems to cause some significant inconveniences and if I understand the following passage from #40227 correctly, it should be possible to ease the restrictions and restore a smoother user experience:

Parts of this proposal are more strict than is technically necessary (for
example, requiring one module, forbidding replace directives). We could relax
these restrictions without breaking compatibility in the future if it seems
expedient. It would be much harder to add restrictions later.

This whole thing is especially tricky since everything needs to be fixed at the root of the issue and even then it's problematic.

If we look at openshift/hypershift#803 for example and its replace directives:
https://github.com/openshift/hypershift/blob/b5e8c84eefb207aaff9be8b9d4f2c606d9019f80/go.mod#L129

it's pointing to https://github.com/kubevirt/containerized-data-importer-api/blob/main/go.mod which has its module name different from its repository. However, a contributor cannot easily fix this because, the change with a following go mod tidy results in something like:

$ git diff
diff --git a/go.mod b/go.mod
index bd68c03..2b66eac 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,11 @@
-module kubevirt.io/containerized-data-importer-api
+module github.com/kubevirt/containerized-data-importer-api

 go 1.17

 require (
        k8s.io/api v0.23.5
        k8s.io/apimachinery v0.23.5
+       kubevirt.io/containerized-data-importer-api v1.47.0
        kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90
 )

diff --git a/go.sum b/go.sum
index 2c6f7f5..709ec7b 100644
--- a/go.sum
+++ b/go.sum
@@ -285,6 +285,8 @@ k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lV
 k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE=
 k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+kubevirt.io/containerized-data-importer-api v1.47.0 h1:fncmQ/2J8MVb3c2snNkUXlBQX472nqCjJJ2AduuMrDc=
+kubevirt.io/containerized-data-importer-api v1.47.0/go.mod h1:yjD8pGZVMCeqcN46JPUQdZ2JwRVoRCOXrTVyNuFvrLo=
 kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 h1:QMrd0nKP0BGbnxTqakhDZAUhGKxPiPiN5gSDqKUmGGc=
 kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90/go.mod h1:018lASpFYBsYN6XwmA2TIrPCx6e0gviTd/ZNtSitKgc=
 sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s=

where kubevirt.io/containerized-data-importer-api v1.47.0 would either need to be changed by hand which might fail in CI or a follow up PR that means extra effort.

tl;dr: if I submitted a proposal to relax the behavior would that be something the team would generally consider?

I would like to add another use case for why applying replace is required:

Summary:

It is required to be able to use BuildInfo (debug.ReadBuildInfo()) as version embedding mechanism which is imo the go1.18 way to go (and will be even cleaner when go build package@version -o somePath will work #52898)

Long version:

I've recently switch version embedding from the previous pattern of ldflags -X foo.version=1.2.3 to using the wonderful new 1.18 BuildInfo and use go install ...@tag to create even the binary distributions of my projects

So for my projects go install project@version is now the only way to create the right binaries

I found a bug in golang.org/x/net that is fixed in a cl but not yet available so I patched it into a fork

https://github.com/fortio/proxy/blob/main/go.mod

replace golang.org/x/net => github.com/fortio/net v0.0.0-20220521233046-9a5c58aa1acd // Has https://go.dev/cl/407454

but I can't install it! so I can't have it show the right version

$ go install fortio.org/proxy@v0.8.3 # works
$ proxy -version # works
14:52:17 I proxy_main.go:67> Fortio Proxy 0.8.3 h1:KdayZCE9EV7NQwOENjnoGtP7gdtSfyIhJ/6hK73hv34= go1.18.2 arm64 darwin starting
0.8.3 h1:KdayZCE9EV7NQwOENjnoGtP7gdtSfyIhJ/6hK73hv34= go1.18.2 arm64 darwin
go	go1.18.2
path	fortio.org/proxy
mod	fortio.org/proxy	v0.8.3	h1:KdayZCE9EV7NQwOENjnoGtP7gdtSfyIhJ/6hK73hv34=
dep	fortio.org/fortio	v1.31.0	h1:5pwFhai6GNFTKiacKlWF3oRjoveFdMCCoqGoSDyFmIA=
dep	github.com/fsnotify/fsnotify	v1.5.4	h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
dep	github.com/google/uuid	v1.3.0	h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
dep	golang.org/x/crypto	v0.0.0-20200622213623-75b288015ac9	h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
dep	golang.org/x/net	v0.0.0-20220425223048-2871e0cb64e4	h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
dep	golang.org/x/sys	v0.0.0-20220412211240-33da011f77ad	h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
dep	golang.org/x/text	v0.3.7	h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
build	-compiler=gc
build	CGO_ENABLED=1
build	CGO_CFLAGS=
build	CGO_CPPFLAGS=
build	CGO_CXXFLAGS=
build	CGO_LDFLAGS=
build	GOARCH=arm64
build	GOOS=darwin
$ go install fortio.org/proxy@v0.9.0
go: fortio.org/proxy@v0.9.0 (in fortio.org/proxy@v0.9.0):
	The go.mod file for the module providing named packages contains one or
	more replace directives. It must not contain directives that would cause
	it to be interpreted differently than if it were the main module.

is there at least a --please-do-it-anyway flag or way to build the binary with the BuildInfo and with a go.mod replacement that I am missing?

in case this helps someone else; it turns out for things like x/net it's easy to use a fork and get go install to work:

change the go.mod in the fork (https://github.com/fortio/net/commit/cd8eba16ed62c07c4269b81d3715b30f18b28a03), eg:

- module golang.org/x/net
+ module github.com/fortio/net

and use that fortio/proxy@0aa96c2

+	"github.com/fortio/net/http2"
-	"golang.org/x/net/http2"

it is a bit painful though because you have to change all the imports, and then revert that back when the upstream change shows up

Seems nobody has pointed this before, but if you follow the multi module repository documentation: https://github.com/golang/go/wiki/Modules#faqs--multi-module-repositories , you won't be able to install your application with go install. It would be good to either document this in the faqs or relax the rules for this case to be handle as described in the faqs.

Is there any proper workaround around this? Or now the installation of projects with a replace inside mod file is not feasible at all?

Unfortunately, I don't believe that there is. go install just simply doesn't work for some projects anymore. It's incredibly frustrating.

Is there any proper workaround around this? Or now the installation of projects with a replace inside mod file is not feasible at all?

The only work around I have figured out is to not use go install git... and manually git clone followed by manual go build/run/install. You could even write a replacement command that would work with go run, setup git clone in a temporary directory, run go build/install, remove the code. Basically hiding this problem from your users. It is lame, but should work and be close to what go actually does.

Why is this issue closed if there is no good solution?

In case there is a good solution, let me know. (I need it.)

It seems it was closed in March with the idea to gather more information. Considering the amount of references to this bug in the mean time, it might be a good idea to reopen the discussion.

For anyone here from a Google search for this error message, try running this command to install a package from a repo ("like npm install -g"):

GO111MODULE=off go get github.com/apenwarr/git-subtrac

(replace github.com/apenwarr/git-subtac with your desired package; that's just an example of the one that led me here today)

It worked as desired:

❯ ls $GOPATH/bin | grep git-subtrac
git-subtrac

Note my go version is go1.18.3 darwin/amd64 which is fairly outdated (latest is 1.19.2), so YMMV.

thanks to Alec over at StackOverflow. more explanation here: https://stackoverflow.com/a/52764550/3793499


For the Go developers, I thought I'd document how I found this error message:

I don't write Go, but I do occasionally run programs in it, and I have a reasonably up-to-date version on my system (go version go1.18.3 darwin/amd64). But the ecosystem is mostly unknown to me. So every time I need to install a package it's a bit of a chore to remember the right $GOPATH settings, check it's actually installed, remember it's go version not go --version, etc. And when something else goes wrong, I have to second guess all that configuration before I know if the error is my fault or not.

Today I found this thread because I wanted to install a global binary (https://github.com/apenwarr/git-subtrac) that I could use on my system. That readme quite reasonably suggests:

git-subtrac is a git extension written in the Go language using the lovely go-git library. If you have Go 1.12 or higher and a project that already uses go modules (contains a go.mod file), you can install the tool like this:

    go install github.com/apenwarr/git-subtrac

If you have an older version of go or don't care about go modules, you can install it like this instead:

    go get -v github.com/apenwarr/git-subtrac

in reality, this is what my session looked like (starting from go version, which took a few attempts to get right πŸ˜„):

❯ go version
go version go1.18.3 darwin/amd64

ok, seems a bit old but not too bad, and this repo is 2 years old, so let's try the instructions from the readme:

❯ go install github.com/apenwarr/git-subtrac                                                                           
go: 'go install' requires a version when current directory is not in a module                                                                           
        Try 'go install github.com/apenwarr/git-subtrac@latest' to install the latest version                       

add @latest? ok, I can try that... and I do like when an error message gives a suggestion :)

❯ go install github.com/apenwarr/git-subtrac@latest
go: github.com/apenwarr/git-subtrac@latest (in github.com/apenwarr/git-subtrac@v0.0.0-20200907023842-6f55d3a89654):
        The go.mod file for the module providing named packages contains one or          
        more replace directives. It must not contain directives that would cause            
        it to be interpreted differently than if it were the main module.                                      

oh no :( I guess that's why it was only a suggestion

*various go help mashing trying to find the answer to how to install a global binary into $GOPATH/bin...*

❯ go get --help
usage: go get [-t] [-u] [-v] [build flags] [packages]
Run 'go help get' for details.

❯ go install --help
usage: go install [build flags] [packages]
Run 'go help install' for details.

❯ go help get | grep -i global

❯ go help install | grep -i global

it's not very helpful :(

*google some words*

Finally find https://stackoverflow.com/a/52764550/3793499 with the answer

@jayconrod

To restart that discussion, we need new information: how is the new go install cmd@version form being used, and would replace directives be helpful or harmful?

Here's a case study: I have a project that relies on some Gtk4 bindings. Those bindings in turn rely on go4.org/unsafe/assume-no-moving-gc which uses build constraints that are manually added as each new Go version is released as a form of an assertion that Go never adds a moving GC. This causes problems, as it means that my app will not build with new versions of Go unless I wait for that module to be updated and then I also update my dependencies to use that new version. It also means that old versions of the app will never build with newer versions of Go.

I got tired of this and added a replace go4.org/unsafe/assume-no-moving-gc => ./internal/empty where internal/empty was a module with nothing in it as a way of getting rid of the assertion. This worked great, until someone tried to install my app using go install at which point it broke due to this issue. Due to the utter lack of viable workarounds, I was forced to remove the replace directive and go back to the manual update process every six months or so.

peebs commented

A more helpful error message I think:

The go.mod file for the module contains replace directives to potentially external code. Try cloning from source and using go build.

I think this message is better because judging the comments in this thread:

  • Its not clear to most why replace directives affect this command.
  • For non go developers, its not obvious that cloning and trying go build is usually the next step when one sees this error.

FWIW I'm pro go install@version respecting in-repo replace directives whether its by just making that change directly or part of some rethink of supporting soft forks.

As of now, I am using the bingo tool to get around this problem. The design of this tool is very good, its a thin wrapper on top of go modules and you can use it to "go install" any binary dependency without getting into the replace directive problem.

I really hope to see a proposal or two here which integrates bingo's concepts directly into golang's tool chain

As of now i saw the below proposal from this issue in bingo but not sure about its implementation status

rsc commented

Someone flagged recent discussion on this closed issue to me privately. As a general note, we don't watch closed issues very carefully - they're closed.

Replace directives remain for local development. If you want to post software that others can go install, it needs to build without the replace directives. If we respected the replace directives during go install foo@latest, then that would create commands that can be installed that way but not using 'go get foo; go install foo' in a module where you are trying to track the version of foo you are using. We are also discussing integrating tools more explicitly with a module in #48429, and again there it would be a serious problem if tools existed that worked with 'go install foo@latest' but not with a regular build graph from another module. (And it is a non-starter for one tool in your module to force the use of its own replace directives. What if you don't want those replace directives? Or what if different tools have conflicting replace directives?)

If you are using dependencies that don't function without replace directives, that may be a sign to rethink the use of those dependencies, or to upstream fixes that make the replace directives unnecessary.

For the specific case of go4.org/unsafe/assume-no-moving-gc, there are two better answers than replace:

  1. (Short-term) Run 'go get go4.org/unsafe/assume-no-moving-gc@latest' in your own module, which will update beyond the version in your dependency, to get a copy that works with the latest version of Go.
  2. (Long-term) Upstream fixes to what is using that package to not use, or upstream fixes to that package to not break at each new Go release.

These work within the ecosystem and keep it healthy. Replace directives fragment the ecosystem and make it brittle. My post The Principles of Versioning in Go has a worked example. It looks at forced package downgrades, but replaces are analogous andhave all the same problems.

We are aware of the problem of go4.org/unsafe/assume-no-moving-gc specifically. In May we worked with the upstream author on a change that makes it no longer break at each new Go release. So if you run 'go get go4.org/unsafe/assume-no-moving-gc@latest' in your own module one last time, you should stop seeing breakages from that package, and you can remove the replace.

maxb commented

Hi @rsc,

Thankyou for the above, but it seems to avoid addressing the key use case I and others in this issue seem to want:

The particular problem is with the statement

Replace directives remain for local development. If you want to post software that others can go install, it needs to build without the replace directives.

which ignores a somewhat common practice:

  • Some project is shipping a CLI executable written in Go.
  • They discover some bug in a third party dependency. They fork it, fix the bug, and use a replace directive to use the forked version until a formally released fix is available.
  • This works perfectly fine in most ways, as being a CLI executable, never depended upon as a library, their module is always the main module, so can use replace directives...
  • ...except when users try to use go install foo/bar/baz@version

It seems like you would class this as a misuse of replace directives based on what you said above?

The problem with that, is that the Go project has given developers an almost perfect tool - replace directives - for dealing with this, so it is unrealistic to think people would not use replace directives in this scenario. AFAIK there are no other options that don't involve needing to change import paths of the forked software in every source file that consumes it, which is inelegant in the extreme!

It's not like the users trying to use go install can go to the developers using the replace directives and ask them not to use them - they'll rightly point out that extreme inelegancy in forking without using replace directives, and decline to do that.

So you end up with unhappy users who have had the lovely convenience of go install dangled in front of them, and then snatched away.

mitar commented

If we respected the replace directives during go install foo@latest, then that would create commands that can be installed that way but not using 'go get foo; go install foo' in a module where you are trying to track the version of foo you are using.

@rsc: My understanding of this issue is that we care about the use case where you use go install outside of a go module. I think what we really are asking for is that go install should just be the same as doing git clone + go install inside the cloned repository. I think this is a well defined behavior. No surprises there. It would make the use case where you fix the program in your fork and wait for upstream to merge it possible.

Installing inside a go module is tricky as you describe. And there is no need to support it there because inside a go module you can just modify go.mod yourself and add the replace directive there.

So, could we get go install to work outside of go modules with repositories which have replace directives?

Some more anecdotal evidence as to why this is an issue:

We are using the k8s client libs, but recently this change was introduced in the openAPI Spec models:
kubernetes/kube-openapi#402
kubernetes/kube-openapi@38767cd

Now we can't upgrade k8s.io/api or k8s.io/client-go because of the model mismatches:

# k8s.io/client-go/applyconfigurations/meta/v1
vendor/k8s.io/client-go/applyconfigurations/meta/v1/unstructured.go:64:38: cannot use doc (variable of type *"github.com/google/gnostic/openapiv2".Document) as *"github.com/google/gnostic-models/openapiv2".Document value in argument to proto.NewOpenAPIData

So we are left with:
1.) Revert (and wait for transient patches)
- kubernetes/kubernetes#118340
- kubernetes-sigs/cli-utils#625
2.) Run a fork (and make the patch ourselves) 😱
3.) Use a replace directive

replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f

Being a Lazy (Pragmatic...Β―_(ツ)_/Β―) Programmer, I chose option 3, as I think most Go Devs would. Now I can upgrade the module I want, without any of the side effects. It just works.

...But now we can't go install the code because of the replace directive. Which is kind of annoying. I would expect go install to honor the replace directive, no different than if I clone the entire repo down and run go install from there.

I would also argue that although the replace directive may have been intended for local development purposes, it is widely used across go projects to circumvent issues exactly like the one I posted here. Dependencies can get messy, and being able to "strategically" use a replace directive when necessary is a nice way to work around (hack) those issues.

Regarding replace directives being "only for local development" reading back to the original vgo and modules proposals I find this about replace.

From vgo tour

If you do identify a problem in a dependency, you need a way to replace it with a fixed copy temporarily. Suppose we want to change something about the behavior of rsc.io/quote. Perhaps we want to work around the problem in rsc.io/sampler, or perhaps we want to do something else. (...)

From Minimal Version Selection

Minimal version selection is very simple. It achieves simplicity by eliminating all flexibility about what the answer must be: the build list is exactly the versions specified in the requirements. A real system needs more flexibility, for example the ability to exclude certain module versions or replace others.

(emphasis mine)

also from Minimal Version Selection, section Who controls your build?

The dependencies of a top-level module must be given some control over the top-level build. B 1.2 needs to be able to make sure it is built with D 1.3 or later, not with D 1.2. Otherwise we end up with the current go get's stale dependency failure mode.
(...)
In the design of module requirements, exclusions, and replacements, I've tried to balance the competing concerns of allowing dependencies enough control to ensure a succesful build without allowing them so much control that they harm the build. Minimum requirements combine without conflict, so it is feasible (even easy) to gather them from all dependencies. But exclusions and replacements can and do conflict, so we allow them to be specified only by the top-level module.

From the modules proposal:

This proposal aims to balance allowing dependencies enough control to ensure a successful build with not allowing them so much control that they break the build. Minimum requirements combine without conflict, so it is feasible (even easy) to gather them from all dependencies, and they make it impossible to pin older versions, as Kubernetes does. Minimal version selection gives the top-level module in the build additional control, allowing it to exclude specific module versions or replace others with different code, but those exclusions and replacements only apply when found in the top-level module, not when the module is a dependency in a larger build.

A module author is therefore in complete control of that moduleβ€˜s build when it is the main program being built, but not in complete control of other users’ builds that depend on the module. I believe this distinction will make this proposal scale to much larger, more distributed code bases than the Bundler/Cargo/Dep approach.

(emphasis mine)

From the Q&A section of the modules proposal:

How does one patch a deep dependency without vendoring?

Response [#24301 (comment) by @kardianos.] By using a replace directive.

PS. I couldn't find any mention in the proposal or in the vgo posts about replace being only appropriate for local builds but AFAICT the proposal did not introduce the distinction between local vs non-local builds, AFAICT that was initially introduced as a consequence of implementation details of go get.

So what do you do if you encounter a replace? lol

I just installed go for the first time to use godo.

go get github.com/digitalocean/godo@v1.106.0

go: go.mod file not found in current directory or any parent directory.
	'go get' is no longer supported outside a module.
	To build and install a command, use 'go install' with a version,
	like 'go install example.com/cmd@latest'
	For more information, see https://golang.org/doc/go-get-install-deprecation
	or run 'go help get' or 'go help install'.

go install github.com/digitalocean/godo@v1.106.0

go: downloading github.com/digitalocean/godo v1.106.0
go: github.com/digitalocean/godo@v1.106.0 (in github.com/digitalocean/godo@v1.106.0):
	The go.mod file for the module providing named packages contains one or
	more replace directives. It must not contain directives that would cause
	it to be interpreted differently than if it were the main module.

I don't get it. (get it?) What's go.mod's deal anyway?

@seanbethard you must git clone that project and run "go install ." within the directory.

@seanbethard you must git clone that project and run "go install ." within the directory.

This would be a lot less of a problem if the go command

  1. Told you to do this when you ran into the error.
  2. Had a command to clone the repo at a given version into a subdirectory of the current directory.

It would still be a problem, but it would be significantly less of one.

icholy commented

I think this bug intentionally exists to prevent people from leaning on replace directives instead of upstreaming their fixes.

As someone who is using a lot of replace directives, this is a quite annoying limitation for go install.

Example: https://github.com/aperturerobotics/bifrost#examples

Here I have to say - well, you could use go install, but we have replace directives, so instead clone it and build. It defeats the purpose of go install.