1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-02-03 13:11:48 +02:00

feat: initial proxy build support (#2129)

* feat: allow to use ModulePath on templates

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* feat: initial proxy build support

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: build

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: main check

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: make it more flexible

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: small improvements

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: copy go.sum

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: root mod proxy

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: test

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: snapshots

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: lint

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: proxy main pkg

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: environment variables

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* test: added some tests to go mod proxy feature

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: improve test

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: linte

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: goreleaser.yml

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: simplify tests

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* test: test build

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: revert unwanted changes

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: allow to run when no mod.suym

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* docs: example

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: not a go module on go 1.15

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* docs: improve docs as per comments

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>
This commit is contained in:
Carlos Alexandro Becker 2021-03-30 21:06:25 -03:00 committed by GitHub
parent 90f2ba6925
commit 8306b946d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 538 additions and 14 deletions

View File

@ -4,6 +4,8 @@ before:
hooks:
- go mod tidy
- ./scripts/completions.sh
gomod:
proxy: true
builds:
- env:
- CGO_ENABLED=0

View File

@ -71,8 +71,10 @@ func (*Builder) WithDefaults(build config.Build) (config.Build, error) {
// Build builds a golang build.
func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error {
if err := checkMain(build); err != nil {
return err
if !ctx.Config.GoMod.Proxy {
if err := checkMain(build); err != nil {
return err
}
}
target, err := newBuildTarget(options.Target)
if err != nil {

View File

@ -4,6 +4,7 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
@ -548,6 +549,47 @@ func TestRunPipeWithoutMainFunc(t *testing.T) {
})
}
func TestRunPipeWithProxiedRepo(t *testing.T) {
folder := testlib.Mktmp(t)
proxied := filepath.Join(folder, "dist/proxy/default")
require.NoError(t, os.MkdirAll(proxied, 0o750))
require.NoError(t, ioutil.WriteFile(
filepath.Join(proxied, "main.go"),
[]byte("// +build: main\npackage main\nimport github.com/goreleaser/goreleaser"),
0o666,
))
require.NoError(t, ioutil.WriteFile(
filepath.Join(proxied, "go.mod"),
[]byte("module foo\nrequire github.com/goreleaser/goreleaser v0.161.1"),
0o666,
))
cmd := exec.Command("go", "mod", "download")
cmd.Dir = proxied
require.NoError(t, cmd.Run())
config := config.Project{
GoMod: config.GoMod{
Proxy: true,
},
Builds: []config.Build{
{
Binary: "foo",
Hooks: config.HookConfig{},
Main: "github.com/goreleaser/goreleaser",
Dir: proxied,
Targets: []string{
runtimeTarget,
},
GoBinary: "go",
},
},
}
ctx := context.New(config)
require.NoError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{
Target: runtimeTarget,
}))
}
func TestRunPipeWithMainFuncNotInMainGoFile(t *testing.T) {
folder := testlib.Mktmp(t)
require.NoError(t, ioutil.WriteFile(

View File

@ -1,14 +1,29 @@
// Package gomod provides go modules utilities, such as template variables and the ability to proxy the module from
// proxy.golang.org.
package gomod
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/apex/log"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
)
const (
go115NotAGoModuleError = "go list -m: not using modules"
go116NotAGoModuleError = "command-line-arguments"
)
// Pipe for env.
type Pipe struct{}
@ -16,14 +31,17 @@ func (Pipe) String() string {
return "loading go mod information"
}
const (
go115NotAGoModuleError = "go list -m: not using modules"
go116NotAGoModuleError = "command-line-arguments"
)
// Default sets the pipe defaults.
func (Pipe) Default(ctx *context.Context) error {
if ctx.Config.GoMod.GoBinary == "" {
ctx.Config.GoMod.GoBinary = "go"
}
return nil
}
// Run the pipe.
func (Pipe) Run(ctx *context.Context) error {
out, err := exec.CommandContext(ctx, "go", "list", "-m").CombinedOutput()
out, err := exec.CommandContext(ctx, ctx.Config.GoMod.GoBinary, "list", "-m").CombinedOutput()
if err != nil {
return fmt.Errorf("failed to get module path: %w: %s", err, string(out))
}
@ -35,5 +53,139 @@ func (Pipe) Run(ctx *context.Context) error {
ctx.ModulePath = result
if !ctx.Config.GoMod.Proxy {
return pipe.Skip("gomod.proxy is disabled")
}
if ctx.Snapshot {
return pipe.ErrSnapshotEnabled
}
return setupProxy(ctx)
}
func setupProxy(ctx *context.Context) error {
for i := range ctx.Config.Builds {
build := &ctx.Config.Builds[i]
if err := proxyBuild(ctx, build); err != nil {
return err
}
}
return nil
}
const goModTpl = `
module {{ .BuildID }}
require {{ .ModulePath }} {{ .Tag }}
`
const mainGoTpl = `
// +build main
package main
import _ "{{ .Main }}"
`
// ErrProxy happens when something goes wrong while proxying the current go module.
type ErrProxy struct {
err error
details string
}
func newErrProxy(err error) error {
return ErrProxy{
err: err,
}
}
func newDetailedErrProxy(err error, details string) error {
return ErrProxy{
err: err,
details: details,
}
}
func (e ErrProxy) Error() string {
out := fmt.Sprintf("failed to proxy module: %v", e.err)
if e.details != "" {
return fmt.Sprintf("%s: %s", out, e.details)
}
return out
}
func (e ErrProxy) Unwrap() error {
return e.err
}
func proxyBuild(ctx *context.Context, build *config.Build) error {
mainPackage := path.Join(ctx.ModulePath, build.Main)
template := tmpl.New(ctx).WithExtraFields(tmpl.Fields{
"Main": mainPackage,
"BuildID": build.ID,
})
log.Infof("proxying %s@%s to build %s", ctx.ModulePath, ctx.Git.CurrentTag, mainPackage)
mod, err := template.Apply(goModTpl)
if err != nil {
return newErrProxy(err)
}
main, err := template.Apply(mainGoTpl)
if err != nil {
return newErrProxy(err)
}
dir := filepath.Join(ctx.Config.Dist, "proxy", build.ID)
log.Debugf("creating needed files")
if err := os.MkdirAll(dir, 0o755); err != nil {
return newErrProxy(err)
}
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(main), 0o666); err != nil {
return newErrProxy(err)
}
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0o666); err != nil {
return newErrProxy(err)
}
if err := copyGoSum("go.sum", filepath.Join(dir, "go.sum")); err != nil {
return newErrProxy(err)
}
log.Debugf("tidying")
cmd := exec.CommandContext(ctx, ctx.Config.GoMod.GoBinary, "mod", "tidy")
cmd.Dir = dir
cmd.Env = append(ctx.Config.GoMod.Env, os.Environ()...)
if out, err := cmd.CombinedOutput(); err != nil {
return newDetailedErrProxy(err, string(out))
}
build.Main = mainPackage
build.Dir = dir
return nil
}
func copyGoSum(src, dst string) error {
r, err := os.OpenFile(src, os.O_RDONLY, 0o666)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
w, err := os.Create(dst)
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, r)
return err
}

View File

@ -1,7 +1,10 @@
package gomod
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/goreleaser/goreleaser/internal/testlib"
@ -12,25 +15,211 @@ import (
func TestRun(t *testing.T) {
ctx := context.New(config.Project{})
require.NoError(t, Pipe{}.Run(ctx))
require.NoError(t, Pipe{}.Default(ctx))
testlib.AssertSkipped(t, Pipe{}.Run(ctx))
require.Equal(t, "github.com/goreleaser/goreleaser", ctx.ModulePath)
}
func TestRunSnapshot(t *testing.T) {
ctx := context.New(config.Project{
GoMod: config.GoMod{
Proxy: true,
},
})
ctx.Snapshot = true
require.NoError(t, Pipe{}.Default(ctx))
testlib.AssertSkipped(t, Pipe{}.Run(ctx))
require.Equal(t, "github.com/goreleaser/goreleaser", ctx.ModulePath)
}
func TestRunOutsideGoModule(t *testing.T) {
require.NoError(t, os.Chdir(t.TempDir()))
dir := testlib.Mktmp(t)
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\nfunc main() {println(0)}"), 0o666))
ctx := context.New(config.Project{})
require.NoError(t, Pipe{}.Default(ctx))
testlib.AssertSkipped(t, Pipe{}.Run(ctx))
require.Empty(t, ctx.ModulePath)
}
func TestRunCommandError(t *testing.T) {
os.Unsetenv("PATH")
require.NoError(t, os.Chdir(t.TempDir()))
ctx := context.New(config.Project{})
require.EqualError(t, Pipe{}.Run(ctx), "failed to get module path: exec: \"go\": executable file not found in $PATH: ")
ctx := context.New(config.Project{
GoMod: config.GoMod{
GoBinary: "not-a-valid-binary",
},
})
require.EqualError(t, Pipe{}.Run(ctx), "failed to get module path: exec: \"not-a-valid-binary\": executable file not found in $PATH: ")
require.Empty(t, ctx.ModulePath)
}
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}
func TestGoModProxy(t *testing.T) {
t.Run("goreleaser", func(t *testing.T) {
dir := testlib.Mktmp(t)
dist := filepath.Join(dir, "dist")
ctx := context.New(config.Project{
Dist: dist,
GoMod: config.GoMod{
Proxy: true,
},
Builds: []config.Build{
{
ID: "foo",
Goos: []string{runtime.GOOS},
Goarch: []string{runtime.GOARCH},
Main: ".",
},
},
})
ctx.Git.CurrentTag = "v0.161.1"
mod := "github.com/goreleaser/goreleaser"
fakeGoModAndSum(t, mod)
require.NoError(t, Pipe{}.Default(ctx))
require.NoError(t, Pipe{}.Run(ctx))
requireGoMod(t, mod, ctx.Git.CurrentTag)
requireMainGo(t, mod)
require.Equal(t, mod, ctx.Config.Builds[0].Main)
require.Equal(t, filepath.Join(dist, "proxy", "foo"), ctx.Config.Builds[0].Dir)
require.Equal(t, mod, ctx.ModulePath)
})
t.Run("nfpm", func(t *testing.T) {
dir := testlib.Mktmp(t)
dist := filepath.Join(dir, "dist")
ctx := context.New(config.Project{
Dist: dist,
GoMod: config.GoMod{
Proxy: true,
},
Builds: []config.Build{
{
ID: "foo",
Goos: []string{runtime.GOOS},
Goarch: []string{runtime.GOARCH},
Main: "./cmd/nfpm",
},
},
})
ctx.Git.CurrentTag = "v2.3.1"
mod := "github.com/goreleaser/nfpm/v2"
fakeGoModAndSum(t, mod)
require.NoError(t, Pipe{}.Default(ctx))
require.NoError(t, Pipe{}.Run(ctx))
requireGoMod(t, mod, ctx.Git.CurrentTag)
requireMainGo(t, mod+"/cmd/nfpm")
require.Equal(t, mod+"/cmd/nfpm", ctx.Config.Builds[0].Main)
require.Equal(t, filepath.Join(dist, "proxy", "foo"), ctx.Config.Builds[0].Dir)
require.Equal(t, mod, ctx.ModulePath)
})
// this repo does not have a go.sum file, which is ok, a project might not have any dependencies
t.Run("no go.sum", func(t *testing.T) {
dir := testlib.Mktmp(t)
dist := filepath.Join(dir, "dist")
ctx := context.New(config.Project{
Dist: dist,
GoMod: config.GoMod{
Proxy: true,
},
Builds: []config.Build{
{
ID: "foo",
Goos: []string{runtime.GOOS},
Goarch: []string{runtime.GOARCH},
},
},
})
ctx.Git.CurrentTag = "v0.0.1"
mod := "github.com/goreleaser/example-mod-proxy"
fakeGoMod(t, mod)
require.NoError(t, Pipe{}.Default(ctx))
require.NoError(t, Pipe{}.Run(ctx))
requireGoMod(t, mod, ctx.Git.CurrentTag)
requireMainGo(t, mod)
require.Equal(t, mod, ctx.Config.Builds[0].Main)
require.Equal(t, filepath.Join(dist, "proxy", "foo"), ctx.Config.Builds[0].Dir)
require.Equal(t, mod, ctx.ModulePath)
})
t.Run("no perms", func(t *testing.T) {
for file, mode := range map[string]os.FileMode{
"go.mod": 0o500,
"go.sum": 0o500,
"main.go": 0o500,
"../../../go.sum": 0o300,
} {
t.Run(file, func(t *testing.T) {
dir := testlib.Mktmp(t)
dist := filepath.Join(dir, "dist")
ctx := context.New(config.Project{
Dist: dist,
GoMod: config.GoMod{
Proxy: true,
},
Builds: []config.Build{
{
ID: "foo",
Goos: []string{runtime.GOOS},
Goarch: []string{runtime.GOARCH},
},
},
})
ctx.Git.CurrentTag = "v0.161.1"
mod := "github.com/goreleaser/goreleaser"
fakeGoModAndSum(t, mod)
require.NoError(t, Pipe{}.Default(ctx))
require.NoError(t, Pipe{}.Run(ctx)) // should succeed at first
// change perms of a file and run again, which should now fail on that file.
require.NoError(t, os.Chmod(filepath.Join(dist, "proxy", "foo", file), mode))
require.ErrorAs(t, Pipe{}.Run(ctx), &ErrProxy{})
})
}
})
}
func requireGoMod(tb testing.TB, module, version string) {
tb.Helper()
mod, err := os.ReadFile("dist/proxy/foo/go.mod")
require.NoError(tb, err)
require.Equal(tb, fmt.Sprintf(`module foo
go 1.16
require %s %s
`, module, version), string(mod))
}
func requireMainGo(tb testing.TB, module string) {
tb.Helper()
main, err := os.ReadFile("dist/proxy/foo/main.go")
require.NoError(tb, err)
require.Equal(tb, fmt.Sprintf(`
// +build main
package main
import _ "%s"
`, module), string(main))
}
func fakeGoModAndSum(tb testing.TB, module string) {
tb.Helper()
fakeGoMod(tb, module)
require.NoError(tb, os.WriteFile("go.sum", []byte("\n"), 0o666))
}
func fakeGoMod(tb testing.TB, module string) {
tb.Helper()
require.NoError(tb, os.WriteFile("go.mod", []byte(fmt.Sprintf("module %s\n", module)), 0o666))
}

View File

@ -38,7 +38,6 @@ type Piper interface {
// BuildPipeline contains all build-related pipe implementations in order.
// nolint:gochecknoglobals
var BuildPipeline = []Piper{
gomod.Pipe{}, // setup gomod-related stuff
env.Pipe{}, // load and validate environment variables
git.Pipe{}, // get and validate git repo state
semver.Pipe{}, // parse current tag to a semver
@ -46,6 +45,7 @@ var BuildPipeline = []Piper{
defaults.Pipe{}, // load default configs
snapshot.Pipe{}, // snapshot version handling
dist.Pipe{}, // ensure ./dist is clean
gomod.Pipe{}, // setup gomod-related stuff
effectiveconfig.Pipe{}, // writes the actual config (with defaults et al set) to dist
changelog.Pipe{}, // builds the release changelog
build.Pipe{}, // build

View File

@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"runtime/debug"
"github.com/goreleaser/goreleaser/cmd"
)
@ -34,5 +35,8 @@ func buildVersion(version, commit, date, builtBy string) string {
if builtBy != "" {
result = fmt.Sprintf("%s\nbuilt by: %s", result, builtBy)
}
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
result = fmt.Sprintf("%s\nmodule version: %s, checksum: %s", result, info.Main.Version, info.Main.Sum)
}
return result
}

View File

@ -610,6 +610,7 @@ type Project struct {
EnvFiles EnvFiles `yaml:"env_files,omitempty"`
Before Before `yaml:",omitempty"`
Source Source `yaml:",omitempty"`
GoMod GoMod `yaml:"gomod,omitempty"`
// this is a hack ¯\_(ツ)_/¯
SingleBuild Build `yaml:"build,omitempty"`
@ -624,6 +625,12 @@ type Project struct {
GiteaURLs GiteaURLs `yaml:"gitea_urls,omitempty"`
}
type GoMod struct {
Proxy bool `yaml:",omitempty"`
Env []string `yaml:",omitempty"`
GoBinary string `yaml:",omitempty"`
}
// Load config file.
func Load(file string) (config Project, err error) {
f, err := os.Open(file) // #nosec

View File

@ -12,6 +12,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/build"
"github.com/goreleaser/goreleaser/internal/pipe/checksums"
"github.com/goreleaser/goreleaser/internal/pipe/docker"
"github.com/goreleaser/goreleaser/internal/pipe/gomod"
"github.com/goreleaser/goreleaser/internal/pipe/milestone"
"github.com/goreleaser/goreleaser/internal/pipe/nfpm"
"github.com/goreleaser/goreleaser/internal/pipe/project"
@ -39,6 +40,7 @@ var Defaulters = []Defaulter{
snapshot.Pipe{},
release.Pipe{},
project.Pipe{},
gomod.Pipe{},
build.Pipe{},
sourcearchive.Pipe{},
archive.Pipe{},

View File

@ -0,0 +1,85 @@
# Building Go modules
With the default configs, you can already build a Go module without issues.
But, if you want to access module information in runtime (e.g. `debug.BuildInfo` or `go version -m $binary`), you'll
need to setup GoReleaser to "proxy" that module before building it.
To do that, you can simply add this to your config:
```yaml
# goreleaser.yml
gomod:
proxy: true
```
In practice, what this does is:
- for each of your builds, create a `dist/proxy/{{ build.id }}`;
- creates a `go.mod` that requires your __main module__ at the __current tag__;
- creates a `main.go` that imports your __main package__;
- copy the project's `go.sum` to that folder.
In which:
- __build.id__: the `id` property in your `build` definition;
- __main module__: is the output of `go list -m`;
- __main package__: is the __main module__ + your build's `main`;
- __current tag__: is the tag that is being built.
So, let's say:
- __main module__: `github.com/goreleaser/nfpm/v2`;
- build's `main`: `./cmd/nfpm/`;
- __current tag__: `v2.5.0`.
GoReleaser will create a `main.go` like:
```go
// +build: main
package main
import _ "github.com/goreleaser/nfpm/v2/cmd/nfpm"
```
a `go.mod` like:
```
module nfpm
require github.com/goreleaser/nfpm/v2 v2.5.0
```
Then, it'll run:
```sh
go mod tidy
```
And, to build, it will use something like:
```shell
go build -o nfpm github.com/goreleaser/nfpm/v2/cmd/nfpm
```
This will resolve the source code from the defined module proxy using `proxy.golang.org`.
Your project's `go.sum` will be used to verify any modules that are downloaded, with `sum.golang.org` "filling in" any gaps.
## Limitations
1. Extra files will still be copied from the current project's root folder and not from the proxy cache;
1. You can't build packages that are not contained in the main module.
## More information
You can find more information about it on the [issue][issue] that originated it and its subsequent [pull request][pr].
Make sure to also read the [relevant documentation][docs] for more options.
[issue]: https://github.com/goreleaser/goreleaser/issues/1354
[pr]: https://github.com/goreleaser/goreleaser/pull/2129
[docs]: /customization/gomod/
## Real example
Source code of a working example can be found at [goreleaser/example-mod-proxy](https://github.com/goreleaser/example-mod-proxy).

View File

@ -0,0 +1,37 @@
---
title: Go Modules
---
GoReleaser has support for creating verifiable builds.
A [verifiable build][vgo] is one that records enough information to be precise about exactly how to repeat it.
All dependencies are loaded via `proxy.golang.org`, and verified against the checksum database `sum.golang.org`.
A GoReleaser-created verifiable build will include module information in the resulting binary, which can be printed using `go version -m mybinary`.
Configuration options available are described bellow.
```yaml
# goreleaser.yml
gomod:
# Proxy a module from proxy.golang.org, making the builds verifiable.
# This will only be effective if running against a tag. Snapshots will ignore this setting.
#
# Default is false.
proxy: true
# If proxy is true, use these environment variables when running `go mod` commands (namely, `go mod tidy`).
# Defaults to `os.Environ()`.
env:
- GOPROXY=https://proxy.golang.org,direct
- GOSUMDB=sum.golang.org
- GOPRIVATE=example.com/blah
# Which Go binary to use.
# Defaults to `go`.
gobinary: go1.15
```
!!! tip
You can use `debug.ReadBuildInfo()` to get the version/checksum/dependencies of the module.
[vgo]: https://research.swtch.com/vgo-repro

View File

@ -62,6 +62,7 @@ nav:
- customization/artifactory.md
- customization/bintray.md
- customization/blob.md
- customization/gomod.md
- customization/build.md
- customization/checksum.md
- customization/publishers.md
@ -87,6 +88,7 @@ nav:
- Cookbooks:
- About: cookbooks/index.md
- Blog Posts: cookbooks/blog-posts.md
- cookbooks//build-go-modules.md
- cookbooks/semantic-release.md
- cookbooks/release-a-library.md
- cookbooks/publish-to-nexus.md