golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.69k stars 17.62k forks source link

x/tools/gopls: improve handling for build tags #29202

Closed stamblerre closed 8 months ago

stamblerre commented 5 years ago

We need to handle build tags correctly in the LSP. If a user is on Linux, they should still see completions, diagnostics, etc. for files tagged with any other OS.

calebstewart commented 2 years ago

@hyangah interesting, it could have been some configuration specific to this project (it's a work project, and I'm not the one who configured it), then, but that seemed to be the best approximation of the suggestions I was seeing online. Admittedly, I'm not exactly sure why it worked for me either, but all I can say is that it did. :eyes:

ahmafi commented 2 years ago

I have a similar problem, I have separate files (config_windows.go, config_linux.go, config_darwin.go) for setting platform-specific configs, and by default, I don't get any code completion on Windows or darwin codes (I'm using VSCode on Linux) and it shows the following message:

No packages found for open file /home/amir/projects/Memor/memor/pkg/config/config_darwin.go: <nil>.
If this file contains build tags, try adding "-tags=<build tag>" to your gopls "buildFlags" configuration (see (https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string).
Otherwise, see the troubleshooting guidelines for help investigating (https://github.com/golang/tools/blob/master/gopls/doc/troubleshooting.md).go list

after adding the following tags I'll get a duplicate declaration error on these files because I'm declaring the same things in these 3 files.

"gopls": {
  "build.buildFlags": ["-tags=linux,windows,darwin"],
}
Michael-F-Ellis commented 2 years ago

@ahmafi I can reproduce the same error messages from vscode/gopls with a trivially simple project using containing 3 files:

common.go

package main

import "fmt"

func common() {
    fmt.Println("This is common")
}

xmain.go

//go:build xmain

package main

import "fmt"

func main() {
    fmt.Println("This is xmain")
    common()
}

ymain.go

//go:build ymain

package main

import "fmt"

func main() {
    fmt.Println("This is ymain")
    common()
}

With no gopls build flags defined, I get the "no packages found" message. If I add the following to settings.json, I get the duplicate declaration error.

"gopls": {
        "build.buildFlags": [
            "xmain",
            "ymain",
        ],
    },

There are no errors reported when I run go vet on the project and I'm able to build and run either variant, e.g. go build -tags xmain without errors.

@stamblerre This is a major limitation for one of my current projects. I'd love to see a solution or a viable workaround.

findleyr commented 2 years ago

@Michael-F-Ellis you should be able to work on xmain OR ymain by setting just one of those two build tags, the same as you do when you run go build -tags xmain. As described in https://github.com/golang/go/issues/49104#issuecomment-1008314902, go vet and go build have the same limitations as gopls: only one set of build tags can be active at a given time.

The critical missing feature for gopls is the ability to edit using multiple sets of build tags simultaneously.

syncjuncture commented 2 years ago

Bump

alexgille commented 2 years ago

Hello,

Is this topic still in progress or should we not expect any improvement regarding this limitation?

Thanks, Best regards

findleyr commented 2 years ago

Hi all, this issue deserves an update (and sorry @alexgille for missing your inquiry above). Let me summarize the current state of things:

We know that support for multiple sets of build tags is one of the most desired features for gopls. Unfortunately, build tags pose a fundamental problem that we can't fix without changing many other things about gopls. Because several of gopls' algorithms rely on having an active graph of go/types.Packages in memory, and go/types relies on the canonical identity of its objects, gopls can't simply process multiple sets of build tags without maintaining two copies of the package graph (because ~everything will at some point in its transitive imports depend on a package for which there are multiple definitions based on build tags). The package graph drives most of gopls' memory usage, so maintaining N copies of the package graph corresponds to approximately an N-fold increase in memory (and often CPU). Given that memory usage is a huge pain point for our users, a solution that depends on throwing memory at the problem will probably not be viable for many users.

Even if that solution is acceptable for some users with small workspaces, it poses a significant burden on the code in an area where we have historically had a lot of bugs: package metadata tracking and package invalidation. I am currently trying to refactor that area of gopls and fix all the bugs. In gopls@v0.9.0 the representation of metadata was refactored, which contributed a piece of the performance improvements observed in that release. In gopls@v0.9.2, I'm trying to fix all the outstanding bugs we know about related to metadata. So here also, we are hesitant to throw more complexity at the problem.

Longer term, an even higher priority for us is to make gopls scale better and load faster. In order to achieve this, we will inevitably need to break our dependence on the package graph. The first step toward this is rewriting key algorithms to be based on data structures that can be incrementally updated at the package level. Once we do this, the problem of handling multiple sets of build tags becomes tractable.

So resolving this issue can't realistically happen soon, but is part of our longer terms goals for gopls.

In the meantime, I think we can still improve the status-quo. For example:

Torwalt commented 2 years ago

For nvim-lspconfig users (with nvim-lsp-installer) following settings worked for me:

lspconfig.gopls.setup {
    settings = {
        gopls = {
            env = {
                GOFLAGS = "-tags=windows,linux,unittest"
            }
        }
    }
}

with files looking like

thing_test.go:

//go:build unittest

package thing_test

and thing.go

package thing

all lsp features work.

eric-burel commented 1 year ago

Cross-posting from https://github.com/golang/vscode-go/issues/1799:

Is there an alternative without relying on setting env variable in VS code settings? VS code doesn't support nesting settings so it's annoying when you mix go server code and go wasm code or more broadly different architectures.

Maybe a top-level comment in the file? This seems to have been mentioned at the begging of this thread: https://github.com/golang/go/issues/29202#issuecomment-488590710, https://github.com/golang/go/issues/29202#issuecomment-498926547

(there are many messages, sorry if it has already been mentioned/answered)

inliquid commented 1 year ago

@eric-burel as a workaround you can open projects in different VS Code instances. WASM client code can be placed somewhere in project dir and can have it's own workspace settings in .vscode/settings.json. I'm using it this way for many years already.

mrunhap commented 1 year ago

For emacs and eglot users:

(setq-default eglot-workspace-configuration
              '((:gopls
                 (:ui.completion.usePlaceholders . t)
                 (:ui.diagnostic.staticcheck . t)
                 (:build.buildFlags . ["-tags" "sometag"]))))
Brandon-M-Hartman commented 1 year ago

Has there been any update on this? Trying to build wasm plugins for my go program and this is obnoxious to deal with.

zzxn commented 1 year ago

This problem is still there, att least a more friendly diagnose tip should be given.

arnevm123 commented 11 months ago

@Torwalt This still gives a compiler: TestFunc redeclared in this block (see details) [DuplicateDecl] , but I guess it's an improvement :smile: Also I can not get this to work with //go:build windows in one file and //go:build !windows in another. (I guess this makes sense because these can't both be true at the same time...)

You can also use this instead of the env, which I find a bit cleaner:

settings = {
    gopls = {
        buildFlags = { "-tags=linux,windows,darwin" },
    },
},
mxygem commented 11 months ago

I currently work in a repo that uses build tags for test files VS Code throws following (inaccurate) error about workspaces when a build flag is present. Setting the tags in the env variables sent to gopls does work around it, I just wanted to inform of another instance of this issue.

This file is within module ".", which is not included in your workspace.
To fix this problem, you can add a go.work file that uses this directory.
See the documentation for more information on setting up your workspace:
https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.go list
findleyr commented 11 months ago

@mxygem can you say more about how you're using build tags in test files? Which build tags, and how are you using them?

solidiquis commented 10 months ago

Omg @Torwalt thank you!!

gopherbot commented 10 months ago

Change https://go.dev/cl/551897 mentions this issue: gopls/internal/lsp/cache: proof of concept for dynamic build tag support

findleyr commented 9 months ago

Hi folks! I'm working on landing improvements to build tag support for gopls@v0.15.0. Specifically, https://go.dev/cl/551897 added logic so that when you open a file constrained by GOOS or GOARCH, gopls will try to find a GOOS/GOARCH combination that includes that file, and will start tracking a build for the new configuration. This means that most gopls functionality will Just Work when, for example, you open up a *_darwin.go file while working on linux.

If you're interested in being an early tester, you can install the first prerelease:

go install golang.org/x/tools/gopls@v0.15.0-pre.1

In the workaround suggested above (https://github.com/golang/go/issues/29202#issuecomment-1233042513), gopls loads only one build configuration, modified to include files for both linux and windows, but this will often lead to spurious build errors for duplicate declarations. Now that it supports multiplexing across multiple builds (#57979), gopls can finally support multiple GOOS/GOARCH combinations simultaneously. If you are testing out the prerelease, please remove any buildFlags configuration from your settings.

If you encounter problems, please let me know and revert to gopls@latest. This is a rather large change to the way gopls operates, and so will require some time to polish before it can be released. Among the potential new bugs, we may encounter new performance edge cases, or possibly conflicting diagnostics from different builds (though I have tried to mitigate both). Additionally, we may require additional UX improvements to make it clearer which build configuration is being used for a given file.

One other area where we could improve build tag support is by automatically handling go version constraints (e.g. //go:build !go1.20). After consulting with the team and Go command experts such as @bcmills and @matloob, we think it is too complex and potentially problematic to support dynamic builds at different Go versions. We also expect that this is an order of magnitude less important than GOOS/GOARCH support -- let us know if you disagree.

meling commented 9 months ago

@findleyr re:

@mxygem can you say more about how you're using build tags in test files? Which build tags, and how are you using them?

I may have a similar scenario to what @mxygem described here.

I have a number of exercises for students, where I provide a template to ensure that our tests are compatible with a student submitted solution. Here is an example:

Template provided to students (rot13.go):

//go:build !solution   // This line is actually removed before we provide it to students.

package cipher

type rot13Reader struct{ r io.Reader }

func (r rot13Reader) Read(p []byte) (n int, err error) {
    return 0, nil
}

Test provided to students (rot13_test.go):

func TestRot13(t *testing.T) { ... }

We also have a solution file (rot13_sol.go) that we don't provide to students:

//go:build solution

package cipher

These files are in the same folder like this:

cipher/
├── rot13.go
├── rot13_sol.go
└── rot13_test.go

From the command line I can test that the template version fails because the solution tag is not set:

% go test
--- FAIL: TestRot13 (0.00s)
    rot13_test.go:33: rot13.Read("Go programming is fun."): (-want +got):
...

And similarly, that the rot13_sol.go version passes the tests because the solution tag is set:

% go test -tags solution
PASS
ok      dat520/lab1/gointro/cipher  0.280s

This approach is convenient because I can keep the template, test, and the solution in same folder.

With VSCode, I can do:

    "gopls": {
        "buildFlags": [
            "-tags=solution"
        ]
    },

To enable compilation of the rot13_sol.go file, and switch to -tags=!solution to enable compilation of the rot13.go file.

However, I'm unaware of any solution to avoid the error:

No packages found for open file ... /lab1/gointro/cipher/rot13.go.
This file may be excluded due to its build tags; try adding "-tags=<build tag>" to your gopls "buildFlags" configuration
See the documentation for more information on working with build tags:
https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string.

Apologies for the long message, and the fact that I didn't take the time to read through the entire issue history. Anyway, I hope this can serve as another example of how tags are being used; I suspect others use it similarly.

findleyr commented 9 months ago

@meling thanks for explaining your use-case. Indeed, there's no way to avoid the error you report in whichever file is excluded by the current configured build tags. In general, there's nothing we can do about this automatically, since it's not possible to guess the set(s) of build tags to use.

Theoretically, since we now support multiple builds per folder, we could accept something like

    "gopls": {
        "buildFlags": [
            [],
            ["-tags=solution"]
        ]
    },

I.e., overload the "buildFlags" setting to accept multiple sets of flags. I'd really like to avoid this if possible, because it complicates the configuration significantly. I wonder how many others would be interested in configurations like this.

inliquid commented 9 months ago

@findleyr is there any point in using "gopls.BuildFlags" instead of "go.buildTags"?

findleyr commented 9 months ago

@inliquid I believe "gopls.buildFlags" affects only gopls, whereas "build.buildFlags" affects all functionality (gopls, test, linter, debugger, etc.), which may or may not be desirable. "go.buildTags" is an older configuration mechanism that was generalized to "buildFlags".

meling commented 9 months ago

@findleyr thanks for your reply.

For my "simple" use-case where there is a binary choice, would it be possible to recognize the tag of the current editor window and just remove the error from source files whose tag is the negation? And then switch if the active editor window changes to the negated tag.

It is not clear to me if dynamically "guessing" based on active editor window is possible.

findleyr commented 9 months ago

@meling you're right, we could do that if there is only one build tag, though probably not anything more complicated. For example, we do have support for tags that are used to isolate scripts, such as //go:build ignore, via the "standaloneTags" configuration, and that looks for files with a build constraint consisting of a single tag.

meling commented 9 months ago

@findleyr thanks for the pointer. Would it be possible to lift the restriction: Gopls considers a file to be a standalone main file if and only if it has package name "main" ?? Maybe then my use case would work. Or would it be preferable to support my use case via a separate configuration option?

Either way I appreciate the work you are doing to improve the plugin and gopls. Thank you so much!

arnevm123 commented 9 months ago

@findleyr I am trying out the new gopls and it seems to work perfectly. The only thing that I'm wondering is if I'm using go to definition on a function that is both declared in a file with a Windows and Linux build flag it goes to the Linux implementation (which makes sense, I'm on Linux). Is it meant to show both definitions (similar to go to implementation) or is this expected behaviour?

findleyr commented 9 months ago

@arnevm123 thanks for testing, and regarding your question this is expected behavior. For each open file there is one "default" build, which is first and foremost the default build on your system, followed by the first match from here. This ordering is somewhat arbitrary, though first class ports are preferred. We do need a fixed order for determinism.

I'd prefer if we could return all definitions, but unfortunately for technical reasons this is really intractable. Note that a file without any build constraints matches all ports. We can't feasibly return every single definition across any port, because this would be much more expensive for little value in the common case. The toolchain (go list and the type checker) only operates on one build context at a time, so to answer questions about multiple build contexts we need to do additional work for each. It is infeasible to do 50x the work (there are around 50 supported ports), so we'd have to try to be smarter by e.g. looking at ignored files in the same directory as the definition to see if there are other declarations of the same symbol. At that point our heuristic is liable to be flaky and/or buggy, and is tightly coupled to the go command (taking us even farther from supporting other build systems). Still, it may be worth pursuing at some point.

For now, I think we should keep it simple. We're going from gopls completely not working to mostly working, albeit with some surprises. Of course we'd greatly appreciate any alternative suggestions, or ways that we could make the current behavior more transparent.

findleyr commented 9 months ago

@meling

Gopls considers a file to be a standalone main file if and only if it has package name "main" ?? Maybe then my use case would work.

I think there's a fundamental difference between scripts, for which there may be many in the same directory, each consisting of a single file, and your use case, for which you want to have at most two versions of your package in the directory. In your use case, I assume you want gopls to resolve packages declarations contained in other unconstrained files in the same directory (for example a common.go file that is shared by both the solution and !solution builds).

meling commented 9 months ago

@meling

Gopls considers a file to be a standalone main file if and only if it has package name "main" ?? Maybe then my use case would work.

I think there's a fundamental difference between scripts, for which there may be many in the same directory, each consisting of a single file, and your use case, for which you want to have at most two versions of your package in the directory. In your use case, I assume you want gopls to resolve packages declarations contained in other unconstrained files in the same directory (for example a common.go file that is shared by both the solution and !solution builds).

Yes, we do have common unconstrained file!

findleyr commented 9 months ago

@meling I think the problem is that we could perhaps make //go:build solution work automatically by creating a new build with -tags=solution overlaid, but then users may be confused when opening a file tagged //go:build (solution || experiment) doesn't work automatically, and we don't want to get into the business of solving constraint equations. The special handling for //go:build ignore, is justifiable because it's an extremely common pattern for scripts.

If enough users want this, we can reconsider. But for now, for the same reasons as https://github.com/golang/go/issues/29202#issuecomment-1889625558, let's keep it simple.

(and thanks again for the feedback!)

findleyr commented 8 months ago

gopls@v0.15.0 will be released soon (likely next week) containing the improvements discussed above. If you're interested to try it out, please install the latest prerelease, which is the release candidate:

go install golang.org/x/tools/gopls@v0.15.0-pre.4

(this differs from -pre.3 by just one minor CL)

With this release, I think working on multiple GOOS and GOARCH combinations is much better, though still not perfect. To follow up, I filed:

This is in addition to @meling's:

I think we're ready to close this issue in favor of the more specific follow-up items above. Please try out the release and let us know if you have feedback, either by commenting on or :+1:'ing one of the issues above, or filing a new issue describing the improvement you'd like to see (or bug you observe). Thanks!

findleyr commented 8 months ago

To clarify one point: the issue title "improve handling for build tags" could certainly be interpreted as also referring to better support for custom build tags, in addition to multiple GOOS/GOARCH. However, the issue description specifically mentions "completions, diagnostics, etc. for files tagged with any other OS", and I think we've achieved that in the v0.15.0 release.

I think better support for user-defined tags is covered by these two issues specifically:

Those issues both address ways that gopls could support working on multiple sets of user-defined tags. In #65089, it is proposed that gopls could automatically detect when there is one tag that separates files in a package. In #65757, it is proposed that users could explicitly configure multiple sets of tags.