Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Support Go Modules builds. #1042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 2, 2021
Merged

Conversation

nevkontakte
Copy link
Member

Fixes #855.

I took the approach of leveraging module support that's built into the go/build package. Doing so has two prerequisites:

  1. go/build.Context instance must not have its file system methods overridden.
  2. ReleaseTags must match the default.

Most of the changes in this PR tend to the #1. We were overriding FS access methods in order to make packages embedded in the VFS available during the build. Now we use two separate contexts chained together to access packages on the real FS and the embedded VFS. This refactoring is useful in the long run to make package loading logic more encapsulated in the build package.

The second prerequisite is somewhat bogus for reasons explained in the build/versionhack package. golang/go#46856 is filed in the upstream for a better behavior, but until it's fixed, we will just work around it.

@nevkontakte nevkontakte requested a review from flimzy July 18, 2021 22:32
@aykevl
Copy link

aykevl commented Jul 20, 2021

Didn't look at the code, but this might be useful: tinygo-org/tinygo#941
In TinyGo, I've basically constructed a modified GOROOT tree (in a temporary directory) and then used go list -json -deps directly.

@nevkontakte
Copy link
Member Author

Thanks! I actually did look at that exact PR at one point, and materializing GOROOT was behind the two of my first attempts to support Modules (this PR representing the third).

This did, however, prove somewhat tricky to get right:

  • Standard library augmentation GopherJS does is more complicated than TinyGo. The latter replaces whole packages (if I remember correctly), while GopherJS rewrites parts of AST. Writing this augmented AST back on disk and making sure it can be correctly parsed again revealed a whole host of problematic edge cases.
  • There are some complications around syscall package and how we deal with OS-dependent code introduce another layer of complexity. Fixing proposal: Limit support to one GOOS value (primarily for stdlib). #693 would eliminate this problem, but at that point it looked a lot like a rabbit hole :)

You are welcome to check out https://github.com/nevkontakte/gopherjs/tree/go-tool/cmd/gopherjs-ng which has some of those experiments if you are curious.

I still like the idea of materializing augmented GOROOT on disk, and I'm going to continue working on it, but I felt like we need a working support for Modules before 1.17, which was rumored to deprecate GOPATH entirely. This is PR is the simplest solution to the problem I found, but it also doesn't address some of the issues which make our build system so complicated and weird.

@nevkontakte
Copy link
Member Author

Also worth mentioning that initially go/build did not support modules and was not meant to, but at some point (not sure when, but probably recently) to did get some level of it by doing that same go list under the hood. So maybe TinyGo implementation could be simplified a bit at this stage 🙂

@flimzy
Copy link
Member

flimzy commented Jul 22, 2021

This seems to fail for me when trying to build a project containing the spf13/cobra library. This can be reproduced by simply building that library, as seen below. I have not done any further investigation yet.

jonhall@ivy:~/src$ git clone [email protected]:spf13/cobra.git
Cloning into 'cobra'...
cd cobraremote: Enumerating objects: 3278, done.
remote: Counting objects: 100% (131/131), done.
remote: Compressing objects: 100% (79/79), done.
remote: Total 3278 (delta 72), reused 90 (delta 50), pack-reused 3147
Receiving objects: 100% (3278/3278), 1.46 MiB | 4.02 MiB/s, done.
Resolving deltas: 100% (2040/2040), done.
jonhall@ivy:~/src$ cd cobra
jonhall@ivy:~/src/cobra$ go build ./...
go: downloading github.com/spf13/viper v1.8.1
go: downloading github.com/spf13/afero v1.6.0
go: downloading github.com/pelletier/go-toml v1.9.3
go: downloading github.com/magiconair/properties v1.8.5
go: downloading github.com/mitchellh/mapstructure v1.4.1
go: downloading golang.org/x/text v0.3.5
jonhall@ivy:~/src/cobra$ gopherjs build ./...
panic: vfs.HasSubDir() is not implemented

goroutine 1 [running]:
github.com/gopherjs/gopherjs/build.vfs.HasSubDir(...)
        /home/jonhall/src/gopherjs/build/vfs.go:28
go/build.(*Context).hasSubdir(0xc0001f35e8, 0xc000284318, 0x11, 0x9c5dae, 0x1, 0x11, 0x1, 0x9c5e3a)
        /usr/local/go/src/go/build/build.go:148 +0x258
go/build.(*Context).Import(0xc0001f35e8, 0x9c5dae, 0x1, 0xc000256df0, 0x1, 0x0, 0x3, 0xaa6dd0, 0xc000270690)
        /usr/local/go/src/go/build/build.go:573 +0x4b69
go/build.(*Context).ImportDir(...)
        /usr/local/go/src/go/build/build.go:470
github.com/kisielk/gotool/internal/load.(*Context).MatchPackagesInFS.func1(0xc000256df0, 0x2, 0xaaa148, 0xc000282750, 0x0, 0x0, 0x0, 0xc000282750)
        /home/jonhall/go/pkg/mod/github.com/kisielk/[email protected]/internal/load/search.go:204 +0x24e
path/filepath.walk(0xc000256df0, 0x2, 0xaaa148, 0xc000282750, 0xc0001f3438, 0x0, 0xd)
        /usr/local/go/src/path/filepath/path.go:418 +0xe7
path/filepath.Walk(0xc000256df0, 0x2, 0xc0001f3438, 0x1, 0x2)
        /usr/local/go/src/path/filepath/path.go:501 +0x113
github.com/kisielk/gotool/internal/load.(*Context).MatchPackagesInFS(0xc0001f35e8, 0xc000256df0, 0x5, 0x3, 0x2, 0x0)
        /home/jonhall/go/pkg/mod/github.com/kisielk/[email protected]/internal/load/search.go:170 +0x175
github.com/kisielk/gotool/internal/load.(*Context).allPackagesInFS(0xc0001f35e8, 0xc000256df0, 0x5, 0x1, 0x2, 0x1)
        /home/jonhall/go/pkg/mod/github.com/kisielk/[email protected]/internal/load/search.go:51 +0x4c
github.com/kisielk/gotool/internal/load.(*Context).ImportPaths(0xc0001f35e8, 0xc0000ba320, 0x1, 0x1, 0xc000284198, 0x11, 0x5)
        /home/jonhall/go/pkg/mod/github.com/kisielk/[email protected]/internal/load/search.go:307 +0x230
github.com/kisielk/gotool.(*Context).importPaths(0xc0001f3720, 0xc0000ba320, 0x1, 0x1, 0xc0000ace60, 0x5, 0x5)
        /home/jonhall/go/pkg/mod/github.com/kisielk/[email protected]/match.go:45 +0x138
github.com/kisielk/gotool.(*Context).ImportPaths(...)
        /home/jonhall/go/pkg/mod/github.com/kisielk/[email protected]/tool.go:32
github.com/gopherjs/gopherjs/build.simpleCtx.Match(...)
        /home/jonhall/src/gopherjs/build/context.go:71
github.com/gopherjs/gopherjs/build.chainedCtx.Match(0xaa57b0, 0xc0000e21e0, 0xaa57b0, 0xc0000e22d0, 0xc0000ba320, 0x1, 0x1, 0x0, 0x0, 0x0)
        /home/jonhall/src/gopherjs/build/context.go:217 +0x13a
main.main.func1.1(0xc0000ba320, 0x1, 0x1, 0xc0000ba110, 0xc0000b89f0, 0xc0000a8200, 0xc0001f3c30, 0xc0001f3c30)
        /home/jonhall/src/gopherjs/tool.go:128 +0x4bc
main.main.func1(0xc0000d0000, 0xc0000ba320, 0x1, 0x1)
        /home/jonhall/src/gopherjs/tool.go:158 +0x1a5
github.com/spf13/cobra.(*Command).execute(0xc0000d0000, 0xc0000ba2f0, 0x1, 0x1, 0xc0000d0000, 0xc0000ba2f0)
        /home/jonhall/go/pkg/mod/github.com/spf13/[email protected]/command.go:856 +0x2c2
github.com/spf13/cobra.(*Command).ExecuteC(0xc0000d1400, 0xc0001f3f38, 0x8, 0x8)
        /home/jonhall/go/pkg/mod/github.com/spf13/[email protected]/command.go:960 +0x375
github.com/spf13/cobra.(*Command).Execute(...)
        /home/jonhall/go/pkg/mod/github.com/spf13/[email protected]/command.go:897
main.main()
        /home/jonhall/src/gopherjs/tool.go:537 +0x1237

@nevkontakte
Copy link
Member Author

I've fixed the panic, which was triggered when expanding patterns containing "/...". I also replaced the gotool package with an actual go tool invocation, since the package predated go modules and didn't know how to match them anyway.

@aykevl
Copy link

aykevl commented Jul 24, 2021

  • Standard library augmentation GopherJS does is more complicated than TinyGo. The latter replaces whole packages (if I remember correctly), while GopherJS rewrites parts of AST. Writing this augmented AST back on disk and making sure it can be correctly parsed again revealed a whole host of problematic edge cases.

Correct for TinyGo. I looked at what GopherJS did and intentionally did not do that. I made sure that only whole packages would be replaced, to simplify the build system. It makes working on the standard library for TinyGo a bit harder, but not by much. And it makes the build system a lot simpler.

That proposal seems like a very sensible thing to do!

What I do in TinyGo is that for every supported target that isn't in Go (that is, WASI and the various baremetal systems) I pick a single GOOS/GOARCH pair for the standard library to use. So for example, for WebAssembly in the browser (which is supported by the Go standard library) I use GOOS=js GOARCH=wasm as usual, but for WASI (which isn't supported by the Go standard library) I use GOOS=linux GOARCH=arm. This works very well in practice, only the syscall package and some packages that interact with the runtime (net, os, reflect, ...) need to be replaced for that to work. This means that most code in existence will think it's a different OS/arch, but in practice it'll usually run just fine. In the (few!) edge cases where it doesn't, they can use the tinygo build tag. One relevant PR in this context: tinygo-org/tinygo#1964.
If this is possible in GopherJS, I can recommend this strategy. It keeps the build process much simpler and (perhaps more importantly) means that supporting a new Go version in GopherJS will be a lot simpler: TinyGo currently needs to do very little to support new Go versions and in fact supports multiple at the same time - this is at least in part due to the policy of only replacing standard library packages as a whole (so that we reduce our reliance on implementation details). For GopherJS, using GOOS=js GOARCH=wasm might be a good strategy. For users which want to differentiate between "true" js/wasm and the GopherJS version, you can simply add a gopherjs build tag (just like there are gc and gccgo build tags to differentiate other compilers).
That said, I do recognize that this would be a really big change which could very well break things as they are.

Also worth mentioning that initially go/build did not support modules and was not meant to, but at some point (not sure when, but probably recently) to did get some level of it by doing that same go list under the hood. So maybe TinyGo implementation could be simplified a bit at this stage 🙂

We currently call go list directly and this works well in practice. I don't think we'll want to change this. See golang/go#38953 for one reason why we're using raw go list.

@aykevl
Copy link

aykevl commented Jul 25, 2021

I think GoScript is getting very much off topic. I'm happy to discuss this in a different place.

@nevkontakte
Copy link
Member Author

@aykevl thanks for your detailed comment, all of what you said makes sense. In general I have high hopes that a lot of GopherJS standard library overlays could be simplified if we try to reuse wasm implementations from the upstream. That said, there's a significant challenge that is unique to GopherJS: we don't have access to the raw memory. So we have to patch any library that does pointer arithmetics, which means it's considerably more packages than runtime, os, net and friends. One of my biggest objectives is to make GopherJS less divergent from upstream, but that's a lot of work and I have only so many hours in a day :)

@paralin I would agree with @aykevl that this discussion would be straying quite far from this PR's subject, so how about we continue it in #gopherjs channel in the Gopher Slack? I like the idea of goscript, but only after having worked on GopherJS compiler for a few months I began to appreciate how many subtle challenges there are if you start considering features like goto, goroutines and runtime packages.

@paralin
Copy link
Contributor

paralin commented Jul 28, 2021

@nevkontakte Getting the following when importing syscall/js - that package is supported for compat w/ wasm code, right?

could not import syscall/js (invalid package name: "")

Thanks for the work on this

@nevkontakte
Copy link
Member Author

@paralin I can't reproduce the error with this minimal example: https://gist.github.com/nevkontakte/56c18ec181a10881ddd84975bd8bc782

Commands I run:

gopherjs build .
node run *.js

@paralin
Copy link
Contributor

paralin commented Jul 31, 2021

@nevkontakte repro here: https://github.com/paralin/gopherjs-repro-1042 just run "go run ./" in the root dir, or run "gopherjs build ./" in the app dir.

@nevkontakte
Copy link
Member Author

@paralin I've fixed the bug, this was indeed a regression introduced by this PR. I also fixed the bug which caused CI to skip syscall/js package in testing.

@flimzy I think this PR is now complete. Could you take another look at it?

…support.

This commit introduces an abstraction for the go/build.Context called
XContext, which will encapsulate GopherJS's customizations to the
package loading and build process.  In future, things like stdlib
augmentation, embedded core packages, etc.  will be hidden behind the
XContext type.

Chaining separate contexts for accessing packages on the real FS and
embedded VFS is one of the prerequisites for using modules support
go/build package has. Since it invokes `go list` command to obtain
module-related information, we can't override FS access methods the way
we were doing previously. So instead we chain two contexts for accessing
packages on FS and VFS respectively.
For packages that come from a module, go/build returns PkgObj path
inside the module root. Using it as-is leads to mixing source code and
binary artifacts and version control mishaps.

We solve the problem by detecting such paths and storing them in a
separate build cache directory. Note that our implementation is much
simpler and not compatible with the Go build cache. We rely upon
go/build returning unique PkgObj paths for different packages and simply
hash the original path to produce a path inside the cache.
go/build disables its module support (arguably incorrectly) whenever
ReleaseTags are set to anything other than default. While such situation
should be rare in practice, GopherJS formally supports being built by a
version other than it's compatible Go version, and this hack ensures
module support doesn't mysteriously disappear in such cases.
Under Go modules pkg.Dir may be initialized as a relative path whenever
a build target is references by a relative path. For consistency, we
convert them all to absolute paths.

This also resolves an issue where "gopherjs build ." creates output
named "..js".
gotool package matching logic was based on the old version of go (most
likely 1.8 or so), and its patterns were not matching packages from
third-party modules. We use `go list` instead to match packages on the
physical file system.

We also provide a shim for VFS-based build contexts using buildutil
package, which should be good enough for the few cases we have.
Under Go Modules package sources a no longer under a single GOPATH and
import paths can't be mapped onto the file system naively. Instead we
have to import the correct package and open the file relative to package
root.

Since we don't know which part of the requested path represents the
package import path, and which is a file within the package, we have to
make a series of guesses until one succeeds.
Go modules will now be used for both gopherjs build and test execution
by default. GOPATH mode is considered deprecated and there's little
value in spending limited CI resources to test it in parallel with
Modules mode.
Vendored directory is not supported outside of module root under Go
Modules. I don't think we really need to test this specifically for
GopherJS since vendoring support is completely provided by go/build and
we indirectly use it when we build standard library anyway.
Vendoring test layout requires GOPATH build mode, though we no longer
use it in the CI workflow by default.
We use https://github.com/gopherjs/todomvc as a simple test case for
either mode, since it is small and easy to debug, but still has a few
external dependencies to exercise Modules infrastructure.

I also added a step that verifies that the output in both modes is
identical. Strictly speaking this is not a requirement, but the current
implementation maintains this invariant, so we might as well test it.
We do want to load it fully from the VFS, since we completely
reimplement the package.
go list only returns this package with GOOS=js GOARCH=wasm. I've
verified that no packages are added or removed from the test set by this
change.
@nevkontakte nevkontakte merged commit 32a0b93 into gopherjs:master Aug 2, 2021
@nevkontakte nevkontakte deleted the build-contexts branch August 2, 2021 13:08
@paralin
Copy link
Contributor

paralin commented Aug 2, 2021

@nevkontakte thanks for looking into it & doing the work to implement modules with gopherjs!

dmitshur added a commit that referenced this pull request Sep 11, 2022
GopherJS gained support for building code in module mode in 2021
(see PR #1042), and it itself didn't have problems being built
in module mode since much earlier.
Recently, it started getting semver tags that are recognized in
module mode like `v1.17.2` and `v1.18.0-beta1` (see issue #847).
Start suggesting the `go install` command to install its latest
release in the README.

(It remains possible to get the latest development version with
something like `go install github.com/gopherjs/gopherjs@HEAD`,
or one of the pre-release versions from `go list -m -versions`
such as `go install github.com/gopherjs/[email protected]`.)

While here, also update to the new shorter Go website domain and
latest Go 1.18 point release.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

module-aware building
4 participants