From d43f84aa3f505ea840a7aec8e816c7630385208a Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 25 Nov 2024 23:00:28 -0300 Subject: [PATCH] refactor(build): preparing to support multiple languages (#5307) This starts laying the foundation for supporting more languages, the first of which will probably be Zig, and then Rust. I already have a zig prototype working in another branch, just raw dogged it to see if it would work, and since it does, now I'll do it piece by piece but with hopefully slightly better code. --- internal/builders/golang/build.go | 111 ++++--- internal/builders/golang/build_test.go | 271 ++++++++++-------- internal/builders/golang/targets.go | 63 ++++ internal/pipe/build/build.go | 31 +- internal/pipe/build/build_test.go | 99 ++++--- internal/pipe/universalbinary/targets.go | 14 + .../pipe/universalbinary/universalbinary.go | 4 +- .../universalbinary/universalbinary_test.go | 6 +- internal/testlib/artifacts.go | 30 ++ internal/tmpl/tmpl.go | 86 +++--- internal/tmpl/tmpl_test.go | 46 ++- pkg/build/build.go | 30 +- pkg/build/build_test.go | 17 ++ 13 files changed, 497 insertions(+), 311 deletions(-) create mode 100644 internal/builders/golang/targets.go create mode 100644 internal/pipe/universalbinary/targets.go create mode 100644 internal/testlib/artifacts.go diff --git a/internal/builders/golang/build.go b/internal/builders/golang/build.go index ea7ee5162..58768a508 100644 --- a/internal/builders/golang/build.go +++ b/internal/builders/golang/build.go @@ -38,6 +38,46 @@ func init() { // Builder is golang builder. type Builder struct{} +// Parse implements build.Builder. +func (b *Builder) Parse(target string) (api.Target, error) { + target = fixTarget(target) + parts := strings.Split(target, "_") + if len(parts) < 2 { + return nil, fmt.Errorf("%s is not a valid build target", target) + } + + goos := parts[0] + goarch := parts[1] + + t := Target{ + Target: target, + Goos: goos, + Goarch: goarch, + } + + if len(parts) > 2 { + extra := parts[2] + switch goarch { + case "amd64": + t.Goamd64 = extra + case "arm64": + t.Goarm64 = extra + case "386": + t.Go386 = extra + case "arm": + t.Goarm = extra + case "mips", "mipsle", "mips64", "mips64le": + t.Gomips = extra + case "ppc64": + t.Goppc64 = extra + case "riscv": + t.Goriscv64 = extra + } + } + + return t, nil +} + // WithDefaults sets the defaults for a golang build and returns it. func (*Builder) WithDefaults(build config.Build) (config.Build, error) { if build.GoBinary == "" { @@ -187,19 +227,21 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti return err } + t := options.Target.(Target) + a := &artifact.Artifact{ Type: artifact.Binary, Path: options.Path, Name: options.Name, - Goos: options.Goos, - Goarch: options.Goarch, - Goamd64: options.Goamd64, - Go386: options.Go386, - Goarm: options.Goarm, - Goarm64: options.Goarm64, - Gomips: options.Gomips, - Goppc64: options.Goppc64, - Goriscv64: options.Goriscv64, + Goos: t.Goos, + Goarch: t.Goarch, + Goamd64: t.Goamd64, + Go386: t.Go386, + Goarm: t.Goarm, + Goarm64: t.Goarm64, + Gomips: t.Gomips, + Goppc64: t.Goppc64, + Goriscv64: t.Goriscv64, Extra: map[string]interface{}{ artifact.ExtraBinary: strings.TrimSuffix(filepath.Base(options.Path), options.Ext), artifact.ExtraExt: options.Ext, @@ -211,12 +253,12 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti a.Type = artifact.CArchive ctx.Artifacts.Add(getHeaderArtifactForLibrary(build, options)) } - if build.Buildmode == "c-shared" && !strings.Contains(options.Target, "wasm") { + if build.Buildmode == "c-shared" && !strings.Contains(t.Target, "wasm") { a.Type = artifact.CShared ctx.Artifacts.Add(getHeaderArtifactForLibrary(build, options)) } - details, err := withOverrides(ctx, build, options) + details, err := withOverrides(ctx, build, t) if err != nil { return err } @@ -238,20 +280,8 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti } } } - env = append( - env, - "GOOS="+options.Goos, - "GOARCH="+options.Goarch, - "GOAMD64="+options.Goamd64, - "GO386="+options.Go386, - "GOARM="+options.Goarm, - "GOARM64="+options.Goarm64, - "GOMIPS="+options.Gomips, - "GOMIPS64="+options.Gomips, - "GOPPC64="+options.Goppc64, - "GORISCV64="+options.Goriscv64, - ) + env = append(env, t.env()...) if v := os.Getenv("GOCACHEPROG"); v != "" { env = append(env, "GOCACHEPROG="+v) } @@ -281,18 +311,10 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti return nil } -func withOverrides(ctx *context.Context, build config.Build, options api.Options) (config.BuildDetails, error) { - optsTarget := options.Goos + "_" + options.Goarch - if extra := options.Goamd64 + options.Go386 + options.Goarm + options.Goarm64 + options.Gomips + options.Goppc64 + options.Goriscv64; extra != "" { - optsTarget += "_" + extra - } - optsTarget = fixTarget(optsTarget) +func withOverrides(ctx *context.Context, build config.Build, target Target) (config.BuildDetails, error) { + optsTarget := target.Target for _, o := range build.BuildDetailsOverrides { - s := o.Goos + "_" + o.Goarch - if extra := o.Goamd64 + o.Go386 + o.Goarm + o.Goarm64 + o.Gomips + o.Goppc64 + o.Goriscv64; extra != "" { - s += "_" + extra - } - overrideTarget, err := tmpl.New(ctx).Apply(s) + overrideTarget, err := tmpl.New(ctx).Apply(formatTarget(o)) if err != nil { return build.BuildDetails, err } @@ -521,20 +543,21 @@ func getHeaderArtifactForLibrary(build config.Build, options api.Options) *artif basePath := filepath.Base(fullPathWithoutExt) fullPath := fullPathWithoutExt + ".h" headerName := basePath + ".h" + t := options.Target.(Target) return &artifact.Artifact{ Type: artifact.Header, Path: fullPath, Name: headerName, - Goos: options.Goos, - Goarch: options.Goarch, - Goamd64: options.Goamd64, - Go386: options.Go386, - Goarm: options.Goarm, - Goarm64: options.Goarm64, - Gomips: options.Gomips, - Goppc64: options.Goppc64, - Goriscv64: options.Goriscv64, + Goos: t.Goos, + Goarch: t.Goarch, + Goamd64: t.Goamd64, + Go386: t.Go386, + Goarm: t.Goarm, + Goarm64: t.Goarm64, + Gomips: t.Gomips, + Goppc64: t.Goppc64, + Goriscv64: t.Goriscv64, Extra: map[string]interface{}{ artifact.ExtraBinary: headerName, artifact.ExtraExt: ".h", diff --git a/internal/builders/golang/build_test.go b/internal/builders/golang/build_test.go index bb730e408..f8c3ba1b6 100644 --- a/internal/builders/golang/build_test.go +++ b/internal/builders/golang/build_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/goreleaser/goreleaser/v2/internal/artifact" + "github.com/goreleaser/goreleaser/v2/internal/experimental" "github.com/goreleaser/goreleaser/v2/internal/testctx" "github.com/goreleaser/goreleaser/v2/internal/testlib" "github.com/goreleaser/goreleaser/v2/internal/tmpl" @@ -33,6 +34,78 @@ var go118FirstClassAdjustedTargets = []string{ "windows_amd64_v1", } +func TestParse(t *testing.T) { + for target, dst := range map[string]Target{ + "linux_amd64": { + Target: "linux_amd64_v1", + Goos: "linux", + Goarch: "amd64", + Goamd64: "v1", + }, + "linux_amd64_v2": { + Target: "linux_amd64_v2", + Goos: "linux", + Goarch: "amd64", + Goamd64: "v2", + }, + "linux_arm": { + Target: "linux_arm_" + experimental.DefaultGOARM(), + Goos: "linux", + Goarch: "arm", + Goarm: experimental.DefaultGOARM(), + }, + "linux_arm_7": { + Target: "linux_arm_7", + Goos: "linux", + Goarch: "arm", + Goarm: "7", + }, + "linux_mips": { + Target: "linux_mips_hardfloat", + Goos: "linux", + Goarch: "mips", + Gomips: "hardfloat", + }, + "linux_mips_softfloat": { + Target: "linux_mips_softfloat", + Goos: "linux", + Goarch: "mips", + Gomips: "softfloat", + }, + "linux_386": { + Target: "linux_386_sse2", + Goos: "linux", + Goarch: "386", + Go386: "sse2", + }, + "linux_386_hardfloat": { + Target: "linux_386_hardfloat", + Goos: "linux", + Goarch: "386", + Go386: "hardfloat", + }, + "linux_arm64": { + Target: "linux_arm64_v8.0", + Goos: "linux", + Goarch: "arm64", + Goarm64: "v8.0", + }, + "linux_arm64_v9.0": { + Target: "linux_arm64_v9.0", + Goos: "linux", + Goarch: "arm64", + Goarm64: "v9.0", + }, + } { + t.Run(target, func(t *testing.T) { + got, err := Default.Parse(target) + require.NoError(t, err) + require.IsType(t, Target{}, got) + require.Equal(t, dst, got.(Target)) + }) + } +} + func TestWithDefaults(t *testing.T) { for name, testcase := range map[string]struct { build config.Build @@ -429,30 +502,14 @@ func TestBuild(t *testing.T) { // injecting some delay here to force inconsistent mod times on bins time.Sleep(2 * time.Second) - parts := strings.Split(target, "_") - goos := parts[0] - goarch := parts[1] - goarm := "" - gomips := "" - if len(parts) > 2 { - if strings.Contains(goarch, "arm") { - goarm = parts[2] - } - if strings.Contains(goarch, "mips") { - gomips = parts[2] - } - } - err := Default.Build(ctx, build, api.Options{ - Target: target, + gtarget, err := Default.Parse(target) + require.NoError(t, err) + require.NoError(t, Default.Build(ctx, build, api.Options{ + Target: gtarget, Name: bin + ext, Path: filepath.Join(folder, "dist", target, bin+ext), - Goos: goos, - Goarch: goarch, - Goarm: goarm, - Gomips: gomips, Ext: ext, - }) - require.NoError(t, err) + })) } list := ctx.Artifacts require.NoError(t, list.Visit(func(a *artifact.Artifact) error { @@ -462,13 +519,14 @@ func TestBuild(t *testing.T) { } return nil })) - require.ElementsMatch(t, list.List(), []*artifact.Artifact{ + expected := []*artifact.Artifact{ { - Name: "bin/foo-v5.6.7", - Path: filepath.ToSlash(filepath.Join("dist", "linux_amd64", "bin", "foo-v5.6.7")), - Goos: "linux", - Goarch: "amd64", - Type: artifact.Binary, + Name: "bin/foo-v5.6.7", + Path: filepath.ToSlash(filepath.Join("dist", "linux_amd64", "bin", "foo-v5.6.7")), + Goos: "linux", + Goarch: "amd64", + Goamd64: "v1", + Type: artifact.Binary, Extra: map[string]interface{}{ artifact.ExtraExt: "", artifact.ExtraBinary: "foo-v5.6.7", @@ -505,11 +563,12 @@ func TestBuild(t *testing.T) { }, }, { - Name: "bin/foo-v5.6.7", - Path: filepath.ToSlash(filepath.Join("dist", "darwin_amd64", "bin", "foo-v5.6.7")), - Goos: "darwin", - Goarch: "amd64", - Type: artifact.Binary, + Name: "bin/foo-v5.6.7", + Path: filepath.ToSlash(filepath.Join("dist", "darwin_amd64", "bin", "foo-v5.6.7")), + Goos: "darwin", + Goarch: "amd64", + Goamd64: "v1", + Type: artifact.Binary, Extra: map[string]interface{}{ artifact.ExtraExt: "", artifact.ExtraBinary: "foo-v5.6.7", @@ -532,11 +591,12 @@ func TestBuild(t *testing.T) { }, }, { - Name: "bin/foo-v5.6.7.exe", - Path: filepath.ToSlash(filepath.Join("dist", "windows_amd64", "bin", "foo-v5.6.7.exe")), - Goos: "windows", - Goarch: "amd64", - Type: artifact.Binary, + Name: "bin/foo-v5.6.7.exe", + Path: filepath.ToSlash(filepath.Join("dist", "windows_amd64", "bin", "foo-v5.6.7.exe")), + Goos: "windows", + Goarch: "amd64", + Goamd64: "v1", + Type: artifact.Binary, Extra: map[string]interface{}{ artifact.ExtraExt: ".exe", artifact.ExtraBinary: "foo-v5.6.7", @@ -557,7 +617,10 @@ func TestBuild(t *testing.T) { "testEnvs": []string{"TEST_T="}, }, }, - }) + } + + got := list.List() + testlib.RequireEqualArtifacts(t, expected, got) modTimes := map[int64]bool{} for _, bin := range ctx.Artifacts.List() { @@ -599,7 +662,7 @@ func TestBuildInvalidEnv(t *testing.T) { }, testctx.WithCurrentTag("5.6.7")) build := ctx.Config.Builds[0] err := Default.Build(ctx, build, api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), Name: build.Binary, Path: filepath.Join("dist", runtimeTarget, build.Binary), Ext: "", @@ -632,7 +695,7 @@ func TestBuildCodeInSubdir(t *testing.T) { }, testctx.WithCurrentTag("5.6.7")) build := ctx.Config.Builds[0] err = Default.Build(ctx, build, api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), Name: build.Binary, Path: filepath.Join("dist", runtimeTarget, build.Binary), Ext: "", @@ -660,7 +723,7 @@ func TestBuildWithDotGoDir(t *testing.T) { }, testctx.WithCurrentTag("5.6.7")) build := ctx.Config.Builds[0] require.NoError(t, Default.Build(ctx, build, api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), Name: build.Binary, Path: filepath.Join("dist", runtimeTarget, build.Binary), Ext: "", @@ -686,7 +749,7 @@ func TestBuildFailed(t *testing.T) { }, }, testctx.WithCurrentTag("5.6.7")) err := Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: "darwin_amd64", + Target: mustParse(t, "darwin_amd64"), }) require.ErrorContains(t, err, `flag provided but not defined: -flag-that-dont-exists-to-force-failure`) require.Empty(t, ctx.Artifacts.List()) @@ -709,7 +772,7 @@ func TestRunInvalidAsmflags(t *testing.T) { }, }, testctx.WithCurrentTag("5.6.7")) err := Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), }) testlib.RequireTemplateError(t, err) } @@ -731,7 +794,7 @@ func TestRunInvalidGcflags(t *testing.T) { }, }, testctx.WithCurrentTag("5.6.7")) err := Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), }) testlib.RequireTemplateError(t, err) } @@ -754,7 +817,7 @@ func TestRunInvalidLdflags(t *testing.T) { }, }, testctx.WithCurrentTag("5.6.7")) err := Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), }) testlib.RequireTemplateError(t, err) } @@ -776,7 +839,7 @@ func TestRunInvalidFlags(t *testing.T) { }, }) err := Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), }) testlib.RequireTemplateError(t, err) } @@ -795,28 +858,28 @@ func TestRunPipeWithoutMainFunc(t *testing.T) { ctx := newCtx(t) ctx.Config.Builds[0].Main = "" require.EqualError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), }), errNoMain{"no-main"}.Error()) }) t.Run("not main.go", func(t *testing.T) { ctx := newCtx(t) ctx.Config.Builds[0].Main = "foo.go" require.ErrorIs(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), }), os.ErrNotExist) }) t.Run("glob", func(t *testing.T) { ctx := newCtx(t) ctx.Config.Builds[0].Main = "." require.EqualError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), }), errNoMain{"no-main"}.Error()) }) t.Run("fixed main.go", func(t *testing.T) { ctx := newCtx(t) ctx.Config.Builds[0].Main = "main.go" require.EqualError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), }), errNoMain{"no-main"}.Error()) }) t.Run("using gomod.proxy", func(t *testing.T) { @@ -827,7 +890,7 @@ func TestRunPipeWithoutMainFunc(t *testing.T) { ctx.Config.Builds[0].UnproxiedDir = "." ctx.Config.Builds[0].UnproxiedMain = "." require.EqualError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), }), errNoMain{"no-main"}.Error()) }) } @@ -848,7 +911,7 @@ func TestBuildTests(t *testing.T) { build, err := Default.WithDefaults(ctx.Config.Builds[0]) require.NoError(t, err) require.NoError(t, Default.Build(ctx, build, api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), })) } @@ -899,7 +962,7 @@ import _ "github.com/goreleaser/goreleaser" }) require.NoError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), })) } @@ -929,19 +992,19 @@ func TestRunPipeWithMainFuncNotInMainGoFile(t *testing.T) { t.Run("empty", func(t *testing.T) { ctx.Config.Builds[0].Main = "" require.NoError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), })) }) t.Run("foo.go", func(t *testing.T) { ctx.Config.Builds[0].Main = "foo.go" require.NoError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), })) }) t.Run("glob", func(t *testing.T) { ctx.Config.Builds[0].Main = "." require.NoError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), })) }) } @@ -1099,7 +1162,7 @@ func TestBuildModTimestamp(t *testing.T) { time.Sleep(2 * time.Second) err := Default.Build(ctx, build, api.Options{ - Target: target, + Target: mustParse(t, runtimeTarget), Name: bin + ext, Path: filepath.Join(folder, "dist", target, bin+ext), Ext: ext, @@ -1131,11 +1194,10 @@ func TestBuildGoBuildLine(t *testing.T) { ) options := api.Options{ Path: ctx.Config.Builds[0].Binary, - Goos: "linux", - Goarch: "amd64", + Target: mustParse(t, "linux_amd64"), } - dets, err := withOverrides(ctx, build, options) + dets, err := withOverrides(ctx, build, options.Target.(Target)) require.NoError(t, err) line, err := buildGoBuildLine( @@ -1298,10 +1360,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: arch, - }, + }, mustParse(t, "linux_"+arch), ) require.NoError(t, err) require.ElementsMatch(t, dets.Ldflags, []string{"overridden"}) @@ -1326,10 +1385,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "amd64", - }, + }, mustParse(t, "linux_amd64"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1358,10 +1414,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: runtime.GOOS, - Goarch: runtime.GOARCH, - }, + }, mustParse(t, runtimeTarget), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1380,10 +1433,7 @@ func TestOverrides(t *testing.T) { Goos: "{{ .Runtime.Goos }", }, }, - }, api.Options{ - Goos: runtime.GOOS, - Goarch: runtime.GOARCH, - }, + }, mustParse(t, runtimeTarget), ) testlib.RequireTemplateError(t, err) }) @@ -1405,11 +1455,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "arm64", - Goarm64: "v8.0", - }, + }, mustParse(t, "linux_arm64_v8.0"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1434,11 +1480,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "arm64", - Goarm64: "v8.0", - }, + }, mustParse(t, "linux_arm64_v8.0"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1464,11 +1506,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "arm", - Goarm: "6", - }, + }, mustParse(t, "linux_arm_6"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1493,11 +1531,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "arm", - Goarm: "6", - }, + }, mustParse(t, "linux_arm_6"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1523,11 +1557,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "mips", - Gomips: "softfloat", - }, + }, mustParse(t, "linux_mips_softfloat"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1552,11 +1582,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "mips", - Gomips: "hardfloat", - }, + }, mustParse(t, "linux_mips_hardfloat"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1582,11 +1608,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "riscv64", - Goriscv64: "rva22u64", - }, + }, mustParse(t, "linux_riscv64_rva22u64"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1612,11 +1634,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "riscv64", - Goriscv64: "rva22u64", - }, + }, mustParse(t, "linux_riscv64_rva22u64"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1642,11 +1660,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "386", - Go386: "sse2", - }, + }, mustParse(t, "linux_386_sse2"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1672,11 +1686,7 @@ func TestOverrides(t *testing.T) { }, }, }, - }, api.Options{ - Goos: "linux", - Goarch: "386", - Go386: "sse2", - }, + }, mustParse(t, "linux_386_sse2"), ) require.NoError(t, err) require.Equal(t, config.BuildDetails{ @@ -1733,7 +1743,7 @@ func TestInvalidGoBinaryTpl(t *testing.T) { }) build := ctx.Config.Builds[0] testlib.RequireTemplateError(t, Default.Build(ctx, build, api.Options{ - Target: runtimeTarget, + Target: mustParse(t, runtimeTarget), Name: build.Binary, Path: filepath.Join("dist", runtimeTarget, build.Binary), Ext: "", @@ -1794,3 +1804,10 @@ func writeTest(t *testing.T, folder string) { 0o666, )) } + +func mustParse(tb testing.TB, target string) Target { + tb.Helper() + got, err := Default.Parse(target) + require.NoError(tb, err) + return got.(Target) +} diff --git a/internal/builders/golang/targets.go b/internal/builders/golang/targets.go new file mode 100644 index 000000000..2cdf23f73 --- /dev/null +++ b/internal/builders/golang/targets.go @@ -0,0 +1,63 @@ +package golang + +import ( + "github.com/goreleaser/goreleaser/v2/internal/tmpl" + "github.com/goreleaser/goreleaser/v2/pkg/config" +) + +func formatTarget(o config.BuildDetailsOverride) string { + target := o.Goos + "_" + o.Goarch + if extra := o.Goamd64 + o.Go386 + o.Goarm + o.Goarm64 + o.Gomips + o.Goppc64 + o.Goriscv64; extra != "" { + target += "_" + extra + } + return target +} + +// Target is a Go build target. +type Target struct { + Target string + Goos string + Goarch string + Goamd64 string + Go386 string + Goarm string + Goarm64 string + Gomips string + Goppc64 string + Goriscv64 string +} + +// Fields implements build.Target. +func (t Target) Fields() map[string]string { + return map[string]string{ + tmpl.KeyOS: t.Goos, + tmpl.KeyArch: t.Goarch, + tmpl.KeyAmd64: t.Goamd64, + tmpl.Key386: t.Go386, + tmpl.KeyArm: t.Goarm, + tmpl.KeyArm64: t.Goarm64, + tmpl.KeyMips: t.Gomips, + tmpl.KeyPpc64: t.Goppc64, + tmpl.KeyRiscv64: t.Goriscv64, + } +} + +// String implements fmt.Stringer. +func (t Target) String() string { + return t.Target +} + +func (t Target) env() []string { + return []string{ + "GOOS=" + t.Goos, + "GOARCH=" + t.Goarch, + "GOAMD64=" + t.Goamd64, + "GO386=" + t.Go386, + "GOARM=" + t.Goarm, + "GOARM64=" + t.Goarm64, + "GOMIPS=" + t.Gomips, + "GOMIPS64=" + t.Gomips, + "GOPPC64=" + t.Goppc64, + "GORISCV64=" + t.Goriscv64, + } +} diff --git a/internal/pipe/build/build.go b/internal/pipe/build/build.go index a97e03e97..14c755143 100644 --- a/internal/pipe/build/build.go +++ b/internal/pipe/build/build.go @@ -165,34 +165,15 @@ func buildOptionsForTarget(ctx *context.Context, build config.Build, target stri return nil, fmt.Errorf("%s is not a valid build target", target) } - goos := parts[0] - goarch := parts[1] - buildOpts := builders.Options{ - Target: target, - Ext: ext, - Goos: goos, - Goarch: goarch, + Ext: ext, } - if len(parts) > 2 { - //nolint:gocritic - if strings.HasPrefix(goarch, "amd64") { - buildOpts.Goamd64 = parts[2] - } else if goarch == "386" { - buildOpts.Go386 = parts[2] - } else if strings.HasPrefix(goarch, "arm64") { - buildOpts.Goarm64 = parts[2] - } else if strings.HasPrefix(goarch, "arm") { - buildOpts.Goarm = parts[2] - } else if strings.HasPrefix(goarch, "mips") { - buildOpts.Gomips = parts[2] - } else if strings.HasPrefix(goarch, "ppc64") { - buildOpts.Goppc64 = parts[2] - } else if goarch == "riscv64" { - buildOpts.Goriscv64 = parts[2] - } + t, err := builders.For(build.Builder).Parse(target) + if err != nil { + return nil, err } + buildOpts.Target = t bin, err := tmpl.New(ctx).WithBuildOptions(buildOpts).Apply(build.Binary) if err != nil { @@ -200,7 +181,7 @@ func buildOptionsForTarget(ctx *context.Context, build config.Build, target stri } name := bin + ext - dir := fmt.Sprintf("%s_%s", build.ID, target) + dir := fmt.Sprintf("%s_%s", build.ID, t) noUnique, err := tmpl.New(ctx).Bool(build.NoUniqueDistDir) if err != nil { return nil, err diff --git a/internal/pipe/build/build_test.go b/internal/pipe/build/build_test.go index 9ba662101..8100ccb88 100644 --- a/internal/pipe/build/build_test.go +++ b/internal/pipe/build/build_test.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "testing" "github.com/goreleaser/goreleaser/v2/internal/artifact" @@ -24,11 +25,34 @@ var ( errFailedDefault = errors.New("fake builder defaults failed") ) +type fakeTarget struct { + target string +} + +// String implements build.Target. +func (f fakeTarget) String() string { + return f.target +} + +// Fields implements build.Target. +func (f fakeTarget) Fields() map[string]string { + os, arch, _ := strings.Cut(f.target, "_") + return map[string]string{ + tmpl.KeyOS: os, + tmpl.KeyArch: arch, + } +} + type fakeBuilder struct { fail bool failDefault bool } +// Parse implements build.Builder. +func (f *fakeBuilder) Parse(target string) (api.Target, error) { + return fakeTarget{target}, nil +} + func (f *fakeBuilder) WithDefaults(build config.Build) (config.Build, error) { if f.failDefault { return build, errFailedDefault @@ -585,7 +609,7 @@ func TestBuildOptionsForTarget(t *testing.T) { testCases := []struct { name string build config.Build - expectedOpts *api.Options + expectedOpts api.Options expectedErr string }{ { @@ -597,13 +621,9 @@ func TestBuildOptionsForTarget(t *testing.T) { "linux_amd64", }, }, - expectedOpts: &api.Options{ - Name: "testbinary", - Path: filepath.Join(tmpDir, "testid_linux_amd64_v1", "testbinary"), - Target: "linux_amd64_v1", - Goos: "linux", - Goarch: "amd64", - Goamd64: "v1", + expectedOpts: api.Options{ + Name: "testbinary", + Path: filepath.Join(tmpDir, "testid_linux_amd64_v1", "testbinary"), }, }, { @@ -615,13 +635,9 @@ func TestBuildOptionsForTarget(t *testing.T) { "linux_amd64", }, }, - expectedOpts: &api.Options{ - Name: "testbinary_linux_amd64", - Path: filepath.Join(tmpDir, "testid_linux_amd64_v1", "testbinary_linux_amd64"), - Target: "linux_amd64_v1", - Goos: "linux", - Goarch: "amd64", - Goamd64: "v1", + expectedOpts: api.Options{ + Name: "testbinary_linux_amd64", + Path: filepath.Join(tmpDir, "testid_linux_amd64_v1", "testbinary_linux_amd64"), }, }, { @@ -634,13 +650,9 @@ func TestBuildOptionsForTarget(t *testing.T) { }, NoUniqueDistDir: `{{ printf "true"}}`, }, - expectedOpts: &api.Options{ - Name: "distpath/linux/amd64/testbinary", - Path: filepath.Join(tmpDir, "distpath", "linux", "amd64", "testbinary"), - Target: "linux_amd64_v1", - Goos: "linux", - Goarch: "amd64", - Goamd64: "v1", + expectedOpts: api.Options{ + Name: "distpath/linux/amd64/testbinary", + Path: filepath.Join(tmpDir, "distpath", "linux", "amd64", "testbinary"), }, }, { @@ -653,13 +665,9 @@ func TestBuildOptionsForTarget(t *testing.T) { }, NoUniqueDistDir: `{{ printf "false"}}`, }, - expectedOpts: &api.Options{ - Name: "testbinary", - Path: filepath.Join(tmpDir, "testid_linux_amd64_v1", "testbinary"), - Target: "linux_amd64_v1", - Goos: "linux", - Goarch: "amd64", - Goamd64: "v1", + expectedOpts: api.Options{ + Name: "testbinary", + Path: filepath.Join(tmpDir, "testid_linux_amd64_v1", "testbinary"), }, }, { @@ -671,13 +679,9 @@ func TestBuildOptionsForTarget(t *testing.T) { "linux_arm_6", }, }, - expectedOpts: &api.Options{ - Name: "testbinary", - Path: filepath.Join(tmpDir, "testid_linux_arm_6", "testbinary"), - Target: "linux_arm_6", - Goos: "linux", - Goarch: "arm", - Goarm: "6", + expectedOpts: api.Options{ + Name: "testbinary", + Path: filepath.Join(tmpDir, "testid_linux_arm_6", "testbinary"), }, }, { @@ -689,13 +693,9 @@ func TestBuildOptionsForTarget(t *testing.T) { "linux_mips_softfloat", }, }, - expectedOpts: &api.Options{ - Name: "testbinary", - Path: filepath.Join(tmpDir, "testid_linux_mips_softfloat", "testbinary"), - Target: "linux_mips_softfloat", - Goos: "linux", - Goarch: "mips", - Gomips: "softfloat", + expectedOpts: api.Options{ + Name: "testbinary", + Path: filepath.Join(tmpDir, "testid_linux_mips_softfloat", "testbinary"), }, }, { @@ -707,13 +707,9 @@ func TestBuildOptionsForTarget(t *testing.T) { "linux_amd64_v3", }, }, - expectedOpts: &api.Options{ - Name: "testbinary", - Path: filepath.Join(tmpDir, "testid_linux_amd64_v3", "testbinary"), - Target: "linux_amd64_v3", - Goos: "linux", - Goarch: "amd64", - Goamd64: "v3", + expectedOpts: api.Options{ + Name: "testbinary", + Path: filepath.Join(tmpDir, "testid_linux_amd64_v3", "testbinary"), }, }, } @@ -728,7 +724,8 @@ func TestBuildOptionsForTarget(t *testing.T) { opts, err := buildOptionsForTarget(ctx, ctx.Config.Builds[0], ctx.Config.Builds[0].Targets[0]) if tc.expectedErr == "" { require.NoError(t, err) - require.Equal(t, tc.expectedOpts, opts) + opts.Target = nil + require.Equal(t, tc.expectedOpts, *opts) } else { require.EqualError(t, err, tc.expectedErr) } diff --git a/internal/pipe/universalbinary/targets.go b/internal/pipe/universalbinary/targets.go new file mode 100644 index 000000000..0fc3c3a03 --- /dev/null +++ b/internal/pipe/universalbinary/targets.go @@ -0,0 +1,14 @@ +package universalbinary + +import "github.com/goreleaser/goreleaser/v2/internal/tmpl" + +type unitarget struct{} + +func (unitarget) String() string { return "darwin_all" } + +func (unitarget) Fields() map[string]string { + return map[string]string{ + tmpl.KeyOS: "darwin", + tmpl.KeyArch: "all", + } +} diff --git a/internal/pipe/universalbinary/universalbinary.go b/internal/pipe/universalbinary/universalbinary.go index 76bcc7cd1..5de5ccf5f 100644 --- a/internal/pipe/universalbinary/universalbinary.go +++ b/internal/pipe/universalbinary/universalbinary.go @@ -55,9 +55,7 @@ func (Pipe) Run(ctx *context.Context) error { for _, unibin := range ctx.Config.UniversalBinaries { g.Go(func() error { opts := build.Options{ - Target: "darwin_all", - Goos: "darwin", - Goarch: "all", + Target: unitarget{}, } if !skips.Any(ctx, skips.PreBuildHooks) { if err := runHook(ctx, &opts, unibin.Hooks.Pre); err != nil { diff --git a/internal/pipe/universalbinary/universalbinary_test.go b/internal/pipe/universalbinary/universalbinary_test.go index 56ad36b1d..240d72a8f 100644 --- a/internal/pipe/universalbinary/universalbinary_test.go +++ b/internal/pipe/universalbinary/universalbinary_test.go @@ -179,7 +179,7 @@ func TestRun(t *testing.T) { }, Post: []config.Hook{ {Cmd: testlib.Touch(post)}, - {Cmd: testlib.ShC(`echo "{{ .Name }} {{ .Os }} {{ .Arch }} {{ .Arm }} {{ .Target }} {{ .Ext }}" > {{ .Path }}.post`), Output: true}, + {Cmd: testlib.ShC(`echo "{{ .Name }} {{ .Os }} {{ .Arch }} {{ .Target }} {{ .Ext }}" > {{ .Path }}.post`), Output: true}, }, }, }, @@ -213,7 +213,7 @@ func TestRun(t *testing.T) { Post: []config.Hook{ {Cmd: testlib.Touch(post)}, { - Cmd: testlib.ShC(`echo "{{ .Name }} {{ .Os }} {{ .Arch }} {{ .Arm }} {{ .Target }} {{ .Ext }}" > {{ .Path }}.post`), + Cmd: testlib.ShC(`echo "{{ .Name }} {{ .Os }} {{ .Arch }} {{ .Target }} {{ .Ext }}" > {{ .Path }}.post`), Output: true, }, }, @@ -312,7 +312,7 @@ func TestRun(t *testing.T) { require.FileExists(t, post) bts, err := os.ReadFile(post) require.NoError(t, err) - require.Contains(t, string(bts), "foo darwin all darwin_all") + require.Contains(t, string(bts), "foo darwin all darwin_all") }) t.Run("failing pre-hook", func(t *testing.T) { diff --git a/internal/testlib/artifacts.go b/internal/testlib/artifacts.go new file mode 100644 index 000000000..c65012968 --- /dev/null +++ b/internal/testlib/artifacts.go @@ -0,0 +1,30 @@ +package testlib + +import ( + "slices" + "strings" + "testing" + + "github.com/goreleaser/goreleaser/v2/internal/artifact" + "github.com/stretchr/testify/require" +) + +func RequireEqualArtifacts(tb testing.TB, expected, got []*artifact.Artifact) { + tb.Helper() + slices.SortFunc(expected, artifactSort) + slices.SortFunc(got, artifactSort) + require.Equal(tb, filenames(expected), filenames(got)) + require.Equal(tb, expected, got) +} + +func artifactSort(a, b *artifact.Artifact) int { + return strings.Compare(a.Path, b.Path) +} + +func filenames(ts []*artifact.Artifact) []string { + result := make([]string, len(ts)) + for i, t := range ts { + result[i] = t.Path + } + return result +} diff --git a/internal/tmpl/tmpl.go b/internal/tmpl/tmpl.go index d1934393c..edb5505e7 100644 --- a/internal/tmpl/tmpl.go +++ b/internal/tmpl/tmpl.go @@ -28,8 +28,21 @@ type Template struct { // Fields that will be available to the template engine. type Fields map[string]interface{} +// Template fields names used in build targets and more. +const ( + KeyOS = "Os" + KeyArch = "Arch" + KeyAmd64 = "Amd64" + Key386 = "I386" + KeyArm = "Arm" + KeyArm64 = "Arm64" + KeyMips = "Mips" + KeyPpc64 = "Ppc64" + KeyRiscv64 = "Riscv64" +) + +// general keys. const ( - // general keys. projectName = "ProjectName" version = "Version" rawVersion = "RawVersion" @@ -65,23 +78,18 @@ const ( modulePath = "ModulePath" releaseNotes = "ReleaseNotes" runtimeK = "Runtime" +) - // artifact-only keys. - osKey = "Os" - arch = "Arch" - amd64 = "Amd64" - go386 = "I386" - arm = "Arm" - arm64 = "Arm64" - mips = "Mips" - ppc64 = "Ppc64" - riscv64 = "Riscv64" +// artifact-only keys. +const ( binary = "Binary" artifactName = "ArtifactName" artifactExt = "ArtifactExt" artifactPath = "ArtifactPath" +) - // build keys. +// build keys. +const ( name = "Name" ext = "Ext" path = "Path" @@ -177,15 +185,15 @@ func (t *Template) WithEnv(e map[string]string) *Template { // WithArtifact populates Fields from the artifact. func (t *Template) WithArtifact(a *artifact.Artifact) *Template { return t.WithExtraFields(Fields{ - osKey: a.Goos, - arch: a.Goarch, - amd64: a.Goamd64, - go386: a.Go386, - arm: a.Goarm, - arm64: a.Goarm64, - mips: a.Gomips, - ppc64: a.Goppc64, - riscv64: a.Goriscv64, + KeyOS: a.Goos, + KeyArch: a.Goarch, + KeyAmd64: a.Goamd64, + Key386: a.Go386, + KeyArm: a.Goarm, + KeyArm64: a.Goarm64, + KeyMips: a.Gomips, + KeyPpc64: a.Goppc64, + KeyRiscv64: a.Goriscv64, binary: artifact.ExtraOr(*a, binary, t.fields[projectName].(string)), artifactName: a.Name, artifactExt: artifact.ExtraOr(*a, artifact.ExtraExt, ""), @@ -198,21 +206,29 @@ func (t *Template) WithBuildOptions(opts build.Options) *Template { } func buildOptsToFields(opts build.Options) Fields { - return Fields{ - target: opts.Target, - ext: opts.Ext, - name: opts.Name, - path: opts.Path, - osKey: opts.Goos, - arch: opts.Goarch, - amd64: opts.Goamd64, - go386: opts.Go386, - arm: opts.Goarm, - arm64: opts.Goarm64, - mips: opts.Gomips, - ppc64: opts.Goppc64, - riscv64: opts.Goriscv64, + f := Fields{ + target: opts.Target.String(), + ext: opts.Ext, + name: opts.Name, + path: opts.Path, + + // set them all to empty, which should prevent breaking templates. + // the .Fields() call will override whichever values are actually + // available. + KeyOS: "", + KeyArch: "", + KeyAmd64: "", + Key386: "", + KeyArm: "", + KeyArm64: "", + KeyMips: "", + KeyPpc64: "", + KeyRiscv64: "", } + for k, v := range opts.Target.Fields() { + f[k] = v + } + return f } // Bool Apply the given string, and converts it to a bool. diff --git a/internal/tmpl/tmpl_test.go b/internal/tmpl/tmpl_test.go index 9f7e31d50..fbf0db45e 100644 --- a/internal/tmpl/tmpl_test.go +++ b/internal/tmpl/tmpl_test.go @@ -460,17 +460,21 @@ func TestInvalidMap(t *testing.T) { } func TestWithBuildOptions(t *testing.T) { + // testtarget doesn ot set riscv64, it still should not fail to compile the template + ts := "{{.Name}}_{{.Path}}_{{.Ext}}_{{.Target}}_{{.Os}}_{{.Arch}}_{{.Amd64}}_{{.Arm}}_{{.Mips}}{{with .Riscv64}}{{.}}{{end}}" out, err := New(testctx.New()).WithBuildOptions(build.Options{ - Name: "name", - Path: "./path", - Ext: ".ext", - Target: "target", - Goos: "os", - Goarch: "arch", - Goamd64: "amd64", - Goarm: "arm", - Gomips: "mips", - }).Apply("{{.Name}}_{{.Path}}_{{.Ext}}_{{.Target}}_{{.Os}}_{{.Arch}}_{{.Amd64}}_{{.Arm}}_{{.Mips}}") + Name: "name", + Path: "./path", + Ext: ".ext", + Target: testTarget{ + Target: "target", + Goos: "os", + Goarch: "arch", + Goamd64: "amd64", + Goarm: "arm", + Gomips: "mips", + }, + }).Apply(ts) require.NoError(t, err) require.Equal(t, "name_./path_.ext_target_os_arch_amd64_arm_mips", out) } @@ -491,3 +495,25 @@ func TestReuseTpl(t *testing.T) { require.NoError(t, err) require.Equal(t, "bar", s3) } + +type testTarget struct { + Target string + Goos string + Goarch string + Goamd64 string + Goarm string + Gomips string +} + +func (t testTarget) String() string { return t.Target } + +func (t testTarget) Fields() map[string]string { + return map[string]string{ + target: t.Target, + KeyOS: t.Goos, + KeyArch: t.Goarch, + KeyAmd64: t.Goamd64, + KeyArm: t.Goarm, + KeyMips: t.Gomips, + } +} diff --git a/pkg/build/build.go b/pkg/build/build.go index f0b30c9cb..7c1f56ad5 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -28,23 +28,27 @@ func For(name string) Builder { // Options to be passed down to a builder. type Options struct { - Name string - Path string - Ext string // with the leading `.`. - Target string - Goos string - Goarch string - Goamd64 string - Go386 string - Goarm string - Goarm64 string - Gomips string - Goppc64 string - Goriscv64 string + Name string + Path string + Ext string // with the leading `.`. + Target Target +} + +// Target represents a build target. +// +// Each Builder implementation can implement its own. +type Target interface { + // String returns the original target. + String() string + + // Fields returns the template fields that will be available for this + // target (e.g. Os, Arch, etc). + Fields() map[string]string } // Builder defines a builder. type Builder interface { WithDefaults(build config.Build) (config.Build, error) Build(ctx *context.Context, build config.Build, options Options) error + Parse(target string) (Target, error) } diff --git a/pkg/build/build_test.go b/pkg/build/build_test.go index 6df0acc82..810bc58ec 100644 --- a/pkg/build/build_test.go +++ b/pkg/build/build_test.go @@ -8,8 +8,25 @@ import ( "github.com/stretchr/testify/require" ) +type dummyTarget struct{} + +// String implements Target. +func (d dummyTarget) String() string { + return "dummy" +} + +// Fields implements Target. +func (d dummyTarget) Fields() map[string]string { + return nil +} + type dummy struct{} +// Parse implements Builder. +func (d *dummy) Parse(string) (Target, error) { + return dummyTarget{}, nil +} + func (*dummy) WithDefaults(build config.Build) (config.Build, error) { return build, nil }