mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-12 03:51:10 +02:00
feat(build): rust support (#5325)
Initial rust support using cargo-zigbuild --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Co-authored-by: Vedant Mohan Goyal <83997633+vedantmgoyal9@users.noreply.github.com>
This commit is contained in:
parent
6cea4fc5b6
commit
d1b5110615
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -16,6 +16,9 @@ internal/builders/zig/all_targets.txt linguist-generated=true
|
||||
internal/builders/zig/error_targets.txt linguist-generated=true
|
||||
internal/builders/zig/testdata/version.txt linguist-generated=true
|
||||
|
||||
internal/builders/rust/all_targets.txt linguist-generated=true
|
||||
internal/builders/rust/testdata/proj/**/* linguist-generated=true
|
||||
|
||||
*.nix.golden linguist-language=Nix
|
||||
*.rb.golden linguist-language=Ruby
|
||||
*.json.golden linguist-language=JSON
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,3 +16,4 @@ manpages
|
||||
output.json
|
||||
.direnv
|
||||
*.pyc
|
||||
.intentionally-empty-file.o
|
||||
|
@ -62,7 +62,7 @@ func TestBuildBrokenProject(t *testing.T) {
|
||||
createFile(t, "main.go", "not a valid go file")
|
||||
cmd := newBuildCmd()
|
||||
cmd.cmd.SetArgs([]string{"--snapshot", "--timeout=1m", "--parallelism=2"})
|
||||
require.EqualError(t, cmd.cmd.Execute(), "failed to parse dir: .: main.go:1:1: expected 'package', found not")
|
||||
require.ErrorContains(t, cmd.cmd.Execute(), "failed to parse dir: .: main.go:1:1: expected 'package', found not")
|
||||
}
|
||||
|
||||
func TestSetupPipeline(t *testing.T) {
|
||||
|
@ -39,6 +39,11 @@ func newInitCmd() *initCmd {
|
||||
log.Info("project contains a 'build.zig', using default zig configuration")
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat("Cargo.toml"); err == nil {
|
||||
root.lang = "rust"
|
||||
log.Info("project contains a 'Cargo.toml', using default rust configuration")
|
||||
return
|
||||
}
|
||||
},
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if _, err := os.Stat(root.config); err == nil {
|
||||
@ -56,6 +61,8 @@ func newInitCmd() *initCmd {
|
||||
switch root.lang {
|
||||
case "zig":
|
||||
example = static.ZigExampleConfig
|
||||
case "rust":
|
||||
example = static.RustExampleConfig
|
||||
case "go":
|
||||
example = static.GoExampleConfig
|
||||
default:
|
||||
@ -88,7 +95,7 @@ func newInitCmd() *initCmd {
|
||||
_ = cmd.RegisterFlagCompletionFunc(
|
||||
"language",
|
||||
cobra.FixedCompletions(
|
||||
[]string{"go", "zig"},
|
||||
[]string{"go", "rust", "zig"},
|
||||
cobra.ShellCompDirectiveDefault,
|
||||
),
|
||||
)
|
||||
|
@ -52,7 +52,7 @@ func TestReleaseBrokenProject(t *testing.T) {
|
||||
createFile(t, "main.go", "not a valid go file")
|
||||
cmd := newReleaseCmd()
|
||||
cmd.cmd.SetArgs([]string{"--snapshot", "--timeout=1m", "--parallelism=2"})
|
||||
require.EqualError(t, cmd.cmd.Execute(), "failed to parse dir: .: main.go:1:1: expected 'package', found not")
|
||||
require.ErrorContains(t, cmd.cmd.Execute(), "failed to parse dir: .: main.go:1:1: expected 'package', found not")
|
||||
}
|
||||
|
||||
func TestReleaseFlags(t *testing.T) {
|
||||
|
2
go.mod
2
go.mod
@ -102,7 +102,7 @@ require (
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
|
27
internal/builders/rust/all_targets.txt
generated
Normal file
27
internal/builders/rust/all_targets.txt
generated
Normal file
@ -0,0 +1,27 @@
|
||||
aarch64-apple-darwin
|
||||
aarch64-pc-windows-gnullvm
|
||||
aarch64-pc-windows-msvc
|
||||
aarch64-unknown-linux-gnu
|
||||
aarch64-unknown-linux-musl
|
||||
arm-unknown-linux-gnueabi
|
||||
arm-unknown-linux-gnueabihf
|
||||
armv7-unknown-linux-gnueabihf
|
||||
i686-pc-windows-gnu
|
||||
i686-pc-windows-msvc
|
||||
i686-unknown-linux-gnu
|
||||
loongarch64-unknown-linux-gnu
|
||||
loongarch64-unknown-linux-musl
|
||||
powerpc-unknown-linux-gnu
|
||||
powerpc64-unknown-linux-gnu
|
||||
powerpc64le-unknown-linux-gnu
|
||||
riscv64gc-unknown-linux-gnu
|
||||
riscv64gc-unknown-linux-musl
|
||||
s390x-unknown-linux-gnu
|
||||
x86_64-apple-darwin
|
||||
x86_64-pc-windows-gnu
|
||||
x86_64-pc-windows-msvc
|
||||
x86_64-unknown-freebsd
|
||||
x86_64-unknown-illumos
|
||||
x86_64-unknown-linux-gnu
|
||||
x86_64-unknown-linux-musl
|
||||
x86_64-unknown-netbsd
|
262
internal/builders/rust/build.go
Normal file
262
internal/builders/rust/build.go
Normal file
@ -0,0 +1,262 @@
|
||||
package rust
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/caarlos0/log"
|
||||
"github.com/goreleaser/goreleaser/v2/internal/artifact"
|
||||
"github.com/goreleaser/goreleaser/v2/internal/gio"
|
||||
"github.com/goreleaser/goreleaser/v2/internal/tmpl"
|
||||
api "github.com/goreleaser/goreleaser/v2/pkg/build"
|
||||
"github.com/goreleaser/goreleaser/v2/pkg/config"
|
||||
"github.com/goreleaser/goreleaser/v2/pkg/context"
|
||||
)
|
||||
|
||||
// Default builder instance.
|
||||
//
|
||||
//nolint:gochecknoglobals
|
||||
var Default = &Builder{}
|
||||
|
||||
// type constraints
|
||||
var (
|
||||
_ api.Builder = &Builder{}
|
||||
_ api.PreparedBuilder = &Builder{}
|
||||
_ api.ConcurrentBuilder = &Builder{}
|
||||
)
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
api.Register("rust", Default)
|
||||
}
|
||||
|
||||
// Builder is golang builder.
|
||||
type Builder struct{}
|
||||
|
||||
// AllowConcurrentBuilds implements build.ConcurrentBuilder.
|
||||
func (b *Builder) AllowConcurrentBuilds() bool { return false }
|
||||
|
||||
// Prepare implements build.PreparedBuilder.
|
||||
func (b *Builder) Prepare(ctx *context.Context, build config.Build) error {
|
||||
for _, target := range build.Targets {
|
||||
out, err := exec.CommandContext(ctx, "rustup", "target", "add", target).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add target %s: %w: %s", target, err, string(out))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse implements build.Builder.
|
||||
func (b *Builder) Parse(target string) (api.Target, error) {
|
||||
parts := strings.Split(target, "-")
|
||||
if len(parts) < 3 {
|
||||
return nil, fmt.Errorf("%s is not a valid build target", target)
|
||||
}
|
||||
|
||||
t := Target{
|
||||
Target: target,
|
||||
Os: parts[2],
|
||||
Vendor: parts[1],
|
||||
Arch: convertToGoarch(parts[0]),
|
||||
}
|
||||
|
||||
if len(parts) > 3 {
|
||||
t.Environment = parts[3]
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// WithDefaults implements build.Builder.
|
||||
func (b *Builder) WithDefaults(build config.Build) (config.Build, error) {
|
||||
log.Warn("you are using the experimental Rust builder")
|
||||
|
||||
if len(build.Targets) == 0 {
|
||||
build.Targets = defaultTargets()
|
||||
}
|
||||
|
||||
if build.GoBinary == "" {
|
||||
build.GoBinary = "cargo"
|
||||
}
|
||||
|
||||
if build.Command == "" {
|
||||
build.Command = "zigbuild"
|
||||
}
|
||||
|
||||
if build.Dir == "" {
|
||||
build.Dir = "."
|
||||
}
|
||||
|
||||
if build.Main != "" {
|
||||
return build, errors.New("main is not used for rust")
|
||||
}
|
||||
|
||||
if len(build.Ldflags) > 0 {
|
||||
return build, errors.New("ldflags is not used for rust")
|
||||
}
|
||||
|
||||
if len(slices.Concat(
|
||||
build.Goos,
|
||||
build.Goarch,
|
||||
build.Goamd64,
|
||||
build.Go386,
|
||||
build.Goarm,
|
||||
build.Goarm64,
|
||||
build.Gomips,
|
||||
build.Goppc64,
|
||||
build.Goriscv64,
|
||||
)) > 0 {
|
||||
return build, errors.New("all go* fields are not used for rust, set targets instead")
|
||||
}
|
||||
|
||||
if len(build.Ignore) > 0 {
|
||||
return build, errors.New("ignore is not used for rust, set targets instead")
|
||||
}
|
||||
|
||||
if build.Buildmode != "" {
|
||||
return build, errors.New("buildmode is not used for rust")
|
||||
}
|
||||
|
||||
if len(build.Tags) > 0 {
|
||||
return build, errors.New("tags is not used for rust")
|
||||
}
|
||||
|
||||
if len(build.Asmflags) > 0 {
|
||||
return build, errors.New("asmtags is not used for rust")
|
||||
}
|
||||
|
||||
if len(build.BuildDetailsOverrides) > 0 {
|
||||
return build, errors.New("overrides is not used for rust")
|
||||
}
|
||||
|
||||
for _, t := range build.Targets {
|
||||
if !isValid(t) {
|
||||
return build, fmt.Errorf("invalid target: %s", t)
|
||||
}
|
||||
}
|
||||
|
||||
return build, nil
|
||||
}
|
||||
|
||||
// Build implements build.Builder.
|
||||
func (b *Builder) Build(ctx *context.Context, build config.Build, options api.Options) error {
|
||||
cargot, err := parseCargo(filepath.Join(build.Dir, "Cargo.toml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: we should probably parse Cargo.toml and handle this better.
|
||||
// Go also has the possibility to build multiple binaries with a single
|
||||
// command, and we currently don't support that either.
|
||||
// We should build something generic enough for both cases, I think.
|
||||
if len(cargot.Workspace.Members) > 0 {
|
||||
return fmt.Errorf("goreleaser does not support cargo workspaces, please set the build 'dir' to one of the workspaces you want to build, e.g. 'dir: %q'", cargot.Workspace.Members[0])
|
||||
}
|
||||
t := options.Target.(Target)
|
||||
a := &artifact.Artifact{
|
||||
Type: artifact.Binary,
|
||||
Path: options.Path,
|
||||
Name: options.Name,
|
||||
Goos: t.Os,
|
||||
Goarch: convertToGoarch(t.Arch),
|
||||
Target: t.Target,
|
||||
Extra: map[string]interface{}{
|
||||
artifact.ExtraBinary: strings.TrimSuffix(filepath.Base(options.Path), options.Ext),
|
||||
artifact.ExtraExt: options.Ext,
|
||||
artifact.ExtraID: build.ID,
|
||||
artifact.ExtraBuilder: "rust",
|
||||
},
|
||||
}
|
||||
|
||||
env := []string{}
|
||||
env = append(env, ctx.Env.Strings()...)
|
||||
|
||||
tpl := tmpl.New(ctx).
|
||||
WithBuildOptions(options).
|
||||
WithEnvS(env).
|
||||
WithArtifact(a)
|
||||
|
||||
cargo, err := tpl.Apply(build.GoBinary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command := []string{
|
||||
cargo,
|
||||
build.Command,
|
||||
"--target=" + t.Target,
|
||||
"--release",
|
||||
}
|
||||
|
||||
for _, e := range build.Env {
|
||||
ee, err := tpl.Apply(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("env %q evaluated to %q", e, ee)
|
||||
if ee != "" {
|
||||
env = append(env, ee)
|
||||
}
|
||||
}
|
||||
|
||||
tpl = tpl.WithEnvS(env)
|
||||
|
||||
flags, err := processFlags(tpl, build.Flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command = append(command, flags...)
|
||||
|
||||
/* #nosec */
|
||||
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
|
||||
cmd.Env = env
|
||||
cmd.Dir = build.Dir
|
||||
log.Debug("running")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", err, string(out))
|
||||
}
|
||||
if s := string(out); s != "" {
|
||||
log.WithField("cmd", command).Info(s)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(options.Path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
realPath := filepath.Join(build.Dir, "target", t.Target, "release", options.Name)
|
||||
if err := gio.Copy(realPath, options.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: move this to outside builder for both go, rust, and zig
|
||||
modTimestamp, err := tpl.Apply(build.ModTimestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gio.Chtimes(a.Path, modTimestamp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Artifacts.Add(a)
|
||||
return nil
|
||||
}
|
||||
|
||||
func processFlags(tpl *tmpl.Template, flags []string) ([]string, error) {
|
||||
var processed []string
|
||||
for _, rawFlag := range flags {
|
||||
flag, err := tpl.Apply(rawFlag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if flag == "" {
|
||||
continue
|
||||
}
|
||||
processed = append(processed, flag)
|
||||
}
|
||||
return processed, nil
|
||||
}
|
212
internal/builders/rust/build_test.go
Normal file
212
internal/builders/rust/build_test.go
Normal file
@ -0,0 +1,212 @@
|
||||
package rust
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goreleaser/goreleaser/v2/internal/artifact"
|
||||
"github.com/goreleaser/goreleaser/v2/internal/testctx"
|
||||
"github.com/goreleaser/goreleaser/v2/internal/testlib"
|
||||
api "github.com/goreleaser/goreleaser/v2/pkg/build"
|
||||
"github.com/goreleaser/goreleaser/v2/pkg/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAllowConcurrentBuilds(t *testing.T) {
|
||||
require.False(t, Default.AllowConcurrentBuilds())
|
||||
}
|
||||
|
||||
func TestWithDefaults(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
build, err := Default.WithDefaults(config.Build{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, config.Build{
|
||||
GoBinary: "cargo",
|
||||
Command: "zigbuild",
|
||||
Dir: ".",
|
||||
Targets: defaultTargets(),
|
||||
}, build)
|
||||
})
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
cases := map[string]config.Build{
|
||||
"main": {
|
||||
Main: "a",
|
||||
},
|
||||
"ldflags": {
|
||||
BuildDetails: config.BuildDetails{
|
||||
Ldflags: []string{"-a"},
|
||||
},
|
||||
},
|
||||
"goos": {
|
||||
Goos: []string{"a"},
|
||||
},
|
||||
"goarch": {
|
||||
Goarch: []string{"a"},
|
||||
},
|
||||
"goamd64": {
|
||||
Goamd64: []string{"a"},
|
||||
},
|
||||
"go386": {
|
||||
Go386: []string{"a"},
|
||||
},
|
||||
"goarm": {
|
||||
Goarm: []string{"a"},
|
||||
},
|
||||
"goarm64": {
|
||||
Goarm64: []string{"a"},
|
||||
},
|
||||
"gomips": {
|
||||
Gomips: []string{"a"},
|
||||
},
|
||||
"goppc64": {
|
||||
Goppc64: []string{"a"},
|
||||
},
|
||||
"goriscv64": {
|
||||
Goriscv64: []string{"a"},
|
||||
},
|
||||
"ignore": {
|
||||
Ignore: []config.IgnoredBuild{{}},
|
||||
},
|
||||
"overrides": {
|
||||
BuildDetailsOverrides: []config.BuildDetailsOverride{{}},
|
||||
},
|
||||
"buildmode": {
|
||||
BuildDetails: config.BuildDetails{
|
||||
Buildmode: "a",
|
||||
},
|
||||
},
|
||||
"tags": {
|
||||
BuildDetails: config.BuildDetails{
|
||||
Tags: []string{"a"},
|
||||
},
|
||||
},
|
||||
"asmflags": {
|
||||
BuildDetails: config.BuildDetails{
|
||||
Asmflags: []string{"a"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for k, v := range cases {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
_, err := Default.WithDefaults(v)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
testlib.CheckPath(t, "rustup")
|
||||
testlib.CheckPath(t, "cargo")
|
||||
|
||||
for _, s := range []string{
|
||||
"rustup default stable",
|
||||
"cargo install --locked cargo-zigbuild",
|
||||
} {
|
||||
args := strings.Fields(s)
|
||||
_, err := exec.Command(args[0], args[1:]...).CombinedOutput()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
modTime := time.Now().AddDate(-1, 0, 0).Round(1 * time.Second).UTC()
|
||||
dist := t.TempDir()
|
||||
ctx := testctx.NewWithCfg(config.Project{
|
||||
Dist: dist,
|
||||
ProjectName: "proj",
|
||||
Env: []string{
|
||||
`TEST_E=1`,
|
||||
},
|
||||
Builds: []config.Build{
|
||||
{
|
||||
ID: "default",
|
||||
Dir: "./testdata/proj/",
|
||||
ModTimestamp: fmt.Sprintf("%d", modTime.Unix()),
|
||||
BuildDetails: config.BuildDetails{
|
||||
Flags: []string{"--locked"},
|
||||
Env: []string{
|
||||
`TEST_T={{- if eq .Os "windows" -}}
|
||||
w
|
||||
{{- else if eq .Os "darwin" -}}
|
||||
d
|
||||
{{- else if eq .Os "linux" -}}
|
||||
l
|
||||
{{- end -}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
build, err := Default.WithDefaults(ctx.Config.Builds[0])
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, Default.Prepare(ctx, build))
|
||||
|
||||
options := api.Options{
|
||||
Name: "proj",
|
||||
Path: filepath.Join(dist, "proj-aarch64-apple-darwin", "proj"),
|
||||
Target: nil,
|
||||
}
|
||||
options.Target, err = Default.Parse("aarch64-apple-darwin")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, Default.Build(ctx, build, options))
|
||||
|
||||
bins := ctx.Artifacts.List()
|
||||
require.Len(t, bins, 1)
|
||||
|
||||
bin := bins[0]
|
||||
require.Equal(t, artifact.Artifact{
|
||||
Name: "proj",
|
||||
Path: filepath.ToSlash(options.Path),
|
||||
Goos: "darwin",
|
||||
Goarch: "arm64",
|
||||
Target: "aarch64-apple-darwin",
|
||||
Type: artifact.Binary,
|
||||
Extra: artifact.Extras{
|
||||
artifact.ExtraBinary: "proj",
|
||||
artifact.ExtraBuilder: "rust",
|
||||
artifact.ExtraExt: "",
|
||||
artifact.ExtraID: "default",
|
||||
},
|
||||
}, *bin)
|
||||
|
||||
require.FileExists(t, bin.Path)
|
||||
fi, err := os.Stat(bin.Path)
|
||||
require.NoError(t, err)
|
||||
require.True(t, modTime.Equal(fi.ModTime()), "inconsistent mod times found when specifying ModTimestamp")
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
_, err := Default.Parse("a-b")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("triplet", func(t *testing.T) {
|
||||
target, err := Default.Parse("aarch64-apple-darwin")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, Target{
|
||||
Target: "aarch64-apple-darwin",
|
||||
Os: "darwin",
|
||||
Arch: "arm64",
|
||||
Vendor: "apple",
|
||||
}, target)
|
||||
})
|
||||
|
||||
t.Run("quadruplet", func(t *testing.T) {
|
||||
target, err := Default.Parse("aarch64-pc-windows-gnullvm")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, Target{
|
||||
Target: "aarch64-pc-windows-gnullvm",
|
||||
Os: "windows",
|
||||
Arch: "arm64",
|
||||
Vendor: "pc",
|
||||
Environment: "gnullvm",
|
||||
}, target)
|
||||
})
|
||||
}
|
23
internal/builders/rust/cargo.go
Normal file
23
internal/builders/rust/cargo.go
Normal file
@ -0,0 +1,23 @@
|
||||
package rust
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Cargo struct {
|
||||
Workspace struct {
|
||||
Members []string
|
||||
}
|
||||
}
|
||||
|
||||
func parseCargo(path string) (Cargo, error) {
|
||||
var cargo Cargo
|
||||
bts, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return cargo, err
|
||||
}
|
||||
err = toml.Unmarshal(bts, &cargo)
|
||||
return cargo, err
|
||||
}
|
13
internal/builders/rust/cargo_test.go
Normal file
13
internal/builders/rust/cargo_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package rust
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseCargo(t *testing.T) {
|
||||
cargo, err := parseCargo("./testdata/workplaces.Cargo.toml")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, cargo.Workspace.Members, 2)
|
||||
}
|
91
internal/builders/rust/targets.go
Normal file
91
internal/builders/rust/targets.go
Normal file
@ -0,0 +1,91 @@
|
||||
package rust
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/goreleaser/goreleaser/v2/internal/tmpl"
|
||||
)
|
||||
|
||||
// tier 1 and tier 2
|
||||
// aarch64-pc-windows-gnullvm is the only tier 3 target added
|
||||
// https://doc.rust-lang.org/rustc/platform-support.html
|
||||
var (
|
||||
//go:embed all_targets.txt
|
||||
allTargetsBts []byte
|
||||
allTargets []string
|
||||
targetsOnce sync.Once
|
||||
)
|
||||
|
||||
const (
|
||||
keyVendor = "Vendor"
|
||||
keyEnvironment = "Environment"
|
||||
)
|
||||
|
||||
// Target is a Rust build target.
|
||||
type Target struct {
|
||||
// The Rust formatted target (arch-vendor-os-env).
|
||||
Target string
|
||||
Os string
|
||||
Arch string
|
||||
Vendor string
|
||||
Environment string
|
||||
}
|
||||
|
||||
// Fields implements build.Target.
|
||||
func (t Target) Fields() map[string]string {
|
||||
return map[string]string{
|
||||
tmpl.KeyOS: t.Os,
|
||||
tmpl.KeyArch: t.Arch,
|
||||
keyEnvironment: t.Environment,
|
||||
keyVendor: t.Vendor,
|
||||
}
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (t Target) String() string {
|
||||
return t.Target
|
||||
}
|
||||
|
||||
func convertToGoarch(s string) string {
|
||||
ss, ok := map[string]string{
|
||||
"aarch64": "arm64",
|
||||
"x86_64": "amd64",
|
||||
"i686": "386",
|
||||
"i586": "386",
|
||||
"i386": "386",
|
||||
"powerpc": "ppc",
|
||||
"powerpc64": "ppc64",
|
||||
"powerpc64le": "ppc64le",
|
||||
"riscv64": "riscv64",
|
||||
"s390x": "s390x",
|
||||
"arm": "arm",
|
||||
"armv7": "arm",
|
||||
"wasm32": "wasm",
|
||||
}[s]
|
||||
if ok {
|
||||
return ss
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func isValid(target string) bool {
|
||||
targetsOnce.Do(func() {
|
||||
allTargets = strings.Split(string(allTargetsBts), "\n")
|
||||
})
|
||||
|
||||
return slices.Contains(allTargets, target)
|
||||
}
|
||||
|
||||
func defaultTargets() []string {
|
||||
return []string{
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-gnu",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-apple-darwin",
|
||||
}
|
||||
}
|
1
internal/builders/rust/testdata/.gitignore
vendored
Normal file
1
internal/builders/rust/testdata/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target
|
1
internal/builders/rust/testdata/proj/.gitignore
generated
vendored
Normal file
1
internal/builders/rust/testdata/proj/.gitignore
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
target
|
7
internal/builders/rust/testdata/proj/Cargo.lock
generated
vendored
Normal file
7
internal/builders/rust/testdata/proj/Cargo.lock
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "proj"
|
||||
version = "0.1.0"
|
6
internal/builders/rust/testdata/proj/Cargo.toml
generated
vendored
Normal file
6
internal/builders/rust/testdata/proj/Cargo.toml
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "proj"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
3
internal/builders/rust/testdata/proj/src/main.rs
generated
vendored
Normal file
3
internal/builders/rust/testdata/proj/src/main.rs
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
3
internal/builders/rust/testdata/workplaces.Cargo.toml
vendored
Normal file
3
internal/builders/rust/testdata/workplaces.Cargo.toml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
members = ["bar", "foo"]
|
||||
resolver = "2"
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
// langs to init.
|
||||
_ "github.com/goreleaser/goreleaser/v2/internal/builders/golang"
|
||||
_ "github.com/goreleaser/goreleaser/v2/internal/builders/rust"
|
||||
_ "github.com/goreleaser/goreleaser/v2/internal/builders/zig"
|
||||
)
|
||||
|
||||
@ -44,11 +45,40 @@ func (Pipe) Run(ctx *context.Context) error {
|
||||
continue
|
||||
}
|
||||
log.WithField("build", build).Debug("building")
|
||||
runPipeOnBuild(ctx, g, build)
|
||||
if err := prepare(ctx, build); err != nil {
|
||||
return err
|
||||
}
|
||||
if allowParallelism(build) {
|
||||
runPipeOnBuild(ctx, g, build)
|
||||
continue
|
||||
}
|
||||
g.Go(func() error {
|
||||
gg := semerrgroup.New(1)
|
||||
runPipeOnBuild(ctx, gg, build)
|
||||
return gg.Wait()
|
||||
})
|
||||
}
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func allowParallelism(build config.Build) bool {
|
||||
conc, ok := builders.For(build.Builder).(builders.ConcurrentBuilder)
|
||||
if !ok {
|
||||
// assume concurrent
|
||||
return true
|
||||
}
|
||||
return conc.AllowConcurrentBuilds()
|
||||
}
|
||||
|
||||
func prepare(ctx *context.Context, build config.Build) error {
|
||||
prep, ok := builders.For(build.Builder).(builders.PreparedBuilder)
|
||||
if !ok {
|
||||
// nothing to do
|
||||
return nil
|
||||
}
|
||||
return prep.Prepare(ctx, build)
|
||||
}
|
||||
|
||||
// Default sets the pipe defaults.
|
||||
func (Pipe) Default(ctx *context.Context) error {
|
||||
ids := ids.New("builds")
|
||||
@ -89,29 +119,33 @@ func buildWithDefaults(ctx *context.Context, build config.Build) (config.Build,
|
||||
func runPipeOnBuild(ctx *context.Context, g semerrgroup.Group, build config.Build) {
|
||||
for _, target := range filter(ctx, build.Targets) {
|
||||
g.Go(func() error {
|
||||
opts, err := buildOptionsForTarget(ctx, build, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !skips.Any(ctx, skips.PreBuildHooks) {
|
||||
if err := runHook(ctx, *opts, build.Env, build.Hooks.Pre); err != nil {
|
||||
return fmt.Errorf("pre hook failed: %w", err)
|
||||
}
|
||||
}
|
||||
if err := doBuild(ctx, build, *opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if !skips.Any(ctx, skips.PostBuildHooks) {
|
||||
if err := runHook(ctx, *opts, build.Env, build.Hooks.Post); err != nil {
|
||||
return fmt.Errorf("post hook failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return buildTarget(ctx, build, target)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildTarget(ctx *context.Context, build config.Build, target string) error {
|
||||
opts, err := buildOptionsForTarget(ctx, build, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !skips.Any(ctx, skips.PreBuildHooks) {
|
||||
if err := runHook(ctx, *opts, build.Env, build.Hooks.Pre); err != nil {
|
||||
return fmt.Errorf("pre hook failed: %w", err)
|
||||
}
|
||||
}
|
||||
if err := doBuild(ctx, build, *opts); err != nil {
|
||||
return fmt.Errorf("build failed: %w\ntarget: %s", err, target)
|
||||
}
|
||||
if !skips.Any(ctx, skips.PostBuildHooks) {
|
||||
if err := runHook(ctx, *opts, build.Env, build.Hooks.Post); err != nil {
|
||||
return fmt.Errorf("post hook failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runHook(ctx *context.Context, opts builders.Options, buildEnv []string, hooks config.Hooks) error {
|
||||
if len(hooks) == 0 {
|
||||
return nil
|
||||
|
@ -213,7 +213,7 @@ func TestRunFullPipeFail(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctx := testctx.NewWithCfg(config, testctx.WithCurrentTag("2.4.5"))
|
||||
require.EqualError(t, Pipe{}.Run(ctx), errFailedBuild.Error())
|
||||
require.ErrorIs(t, Pipe{}.Run(ctx), errFailedBuild)
|
||||
require.Empty(t, ctx.Artifacts.List())
|
||||
require.FileExists(t, pre)
|
||||
}
|
||||
|
@ -12,3 +12,8 @@ var GoExampleConfig []byte
|
||||
//
|
||||
//go:embed config.zig.yaml
|
||||
var ZigExampleConfig []byte
|
||||
|
||||
// RustExampleConfig is the config used within goreleaser init --lang rust.
|
||||
//
|
||||
//go:embed config.rust.yaml
|
||||
var RustExampleConfig []byte
|
||||
|
54
internal/static/config.rust.yaml
Normal file
54
internal/static/config.rust.yaml
Normal file
@ -0,0 +1,54 @@
|
||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||
# Make sure to check the documentation at https://goreleaser.com
|
||||
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/need to use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# if you don't do these things before calling goreleaser, it might be a
|
||||
# good idea to do them here:
|
||||
- rustup default stable
|
||||
- cargo install --locked cargo-zigbuild
|
||||
- cargo fetch --locked
|
||||
|
||||
builds:
|
||||
- builder: rust
|
||||
targets:
|
||||
- x86_64-unknown-linux-gnu
|
||||
- x86_64-apple-darwin
|
||||
- x86_64-pc-windows-gnu
|
||||
- aarch64-unknown-linux-gnu
|
||||
- aarch64-apple-darwin
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
release:
|
||||
footer: >-
|
||||
|
||||
---
|
||||
|
||||
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
@ -52,3 +52,15 @@ type Builder interface {
|
||||
Build(ctx *context.Context, build config.Build, options Options) error
|
||||
Parse(target string) (Target, error)
|
||||
}
|
||||
|
||||
// PreparedBuilder can be implemented to run something before all the actual
|
||||
// builds happen.
|
||||
type PreparedBuilder interface {
|
||||
Prepare(ctx *context.Context, build config.Build) error
|
||||
}
|
||||
|
||||
// ConcurrentBuilder can be implemented to indicate whether or not this builder
|
||||
// support concurrent builds.
|
||||
type ConcurrentBuilder interface {
|
||||
AllowConcurrentBuilds() bool
|
||||
}
|
||||
|
@ -511,7 +511,7 @@ type Build struct {
|
||||
Main string `yaml:"main,omitempty" json:"main,omitempty"`
|
||||
Binary string `yaml:"binary,omitempty" json:"binary,omitempty"`
|
||||
Hooks BuildHookConfig `yaml:"hooks,omitempty" json:"hooks,omitempty"`
|
||||
Builder string `yaml:"builder,omitempty" json:"builder,omitempty" jsonschema:"enum=,enum=go,enum=zig"`
|
||||
Builder string `yaml:"builder,omitempty" json:"builder,omitempty" jsonschema:"enum=,enum=go,enum=rust,enum=zig"`
|
||||
ModTimestamp string `yaml:"mod_timestamp,omitempty" json:"mod_timestamp,omitempty"`
|
||||
Skip string `yaml:"skip,omitempty" json:"skip,omitempty" jsonschema:"oneof_type=string;boolean"`
|
||||
GoBinary string `yaml:"gobinary,omitempty" json:"gobinary,omitempty"`
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Builds
|
||||
# Builds (Go)
|
||||
|
||||
Builds can be customized in multiple ways.
|
||||
You can specify for which `GOOS`, `GOARCH` and `GOARM` binaries are built
|
||||
@ -262,7 +262,7 @@ builds:
|
||||
dir: go
|
||||
|
||||
# Builder allows you to use a different build implementation.
|
||||
# Valid options are: `go`, `zig`, and `prebuilt` (pro-only).
|
||||
# Valid options are: `go`, `rust`, `zig`, and `prebuilt` (pro-only).
|
||||
#
|
||||
# Default: 'go'.
|
||||
builder: prebuilt
|
||||
|
90
www/docs/customization/rust-builds.md
Normal file
90
www/docs/customization/rust-builds.md
Normal file
@ -0,0 +1,90 @@
|
||||
# Builds (Rust)
|
||||
|
||||
<!-- md:version v2.5-unreleased -->
|
||||
|
||||
<!-- md:alpha -->
|
||||
|
||||
You can now build Rust binaries using `cargo zigbuild` and GoReleaser!
|
||||
|
||||
Simply set the `builder` to `rust`, for instance:
|
||||
|
||||
```yaml title=".goreleaser.yaml"
|
||||
builds:
|
||||
# You can have multiple builds defined as a yaml list
|
||||
- #
|
||||
# ID of the build.
|
||||
#
|
||||
# Default: Project directory name.
|
||||
id: "my-build"
|
||||
|
||||
# Use rust.
|
||||
builder: rust
|
||||
|
||||
# Binary name.
|
||||
# Can be a path (e.g. `bin/app`) to wrap the binary in a directory.
|
||||
#
|
||||
# Default: Project directory name.
|
||||
binary: program
|
||||
|
||||
# List of targets to be built, in Rust's format.
|
||||
targets:
|
||||
- x86_64-apple-darwin
|
||||
- x86_64-pc-windows-gnu
|
||||
|
||||
# Path to project's (sub)directory containing the code.
|
||||
# This is the working directory for the Zig build command(s).
|
||||
#
|
||||
# Default: '.'.
|
||||
dir: my-app
|
||||
|
||||
# Set a specific zig binary to use when building.
|
||||
# It is safe to ignore this option in most cases.
|
||||
#
|
||||
# Default: "cargo".
|
||||
# Templates: allowed.
|
||||
gobinary: "cross"
|
||||
|
||||
# Sets the command to run to build.
|
||||
# Can be useful if you want to build tests, for example,
|
||||
# in which case you can set this to "test".
|
||||
# It is safe to ignore this option in most cases.
|
||||
#
|
||||
# Default: zigbuild.
|
||||
command: build
|
||||
|
||||
# Custom flags.
|
||||
#
|
||||
# Templates: allowed.
|
||||
flags:
|
||||
- --release
|
||||
```
|
||||
|
||||
Some options are not supported yet[^fail], but it should be usable at least for
|
||||
simple projects already!
|
||||
|
||||
GoReleaser will run `rustup target add` for each defined target.
|
||||
You can use before hooks to install `cargo-zigbuild`.
|
||||
If you want to use `cargo-cross` instead, you can make sure it is installed and
|
||||
then make few changes:
|
||||
|
||||
```yaml title=".goreleaser.yaml"
|
||||
builds:
|
||||
- # Use Rust zigbuild
|
||||
builder: rust
|
||||
gobinary: cross # TODO: rename gobinary to something more generic, like 'builder_binary' maybe?
|
||||
command: build
|
||||
targets:
|
||||
- x86_64-apple-darwin
|
||||
- x86_64-pc-windows-gnu
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
GoReleaser will translate Rust's Os/Arch triple into a GOOS/GOARCH pair, so
|
||||
templates should work the same as before.
|
||||
The original target name is available in templates as `.Target`, and so are
|
||||
`.Vendor` and `.Environment`.
|
||||
|
||||
[^fail]:
|
||||
GoReleaser will error if you try to use them. Give it a try with
|
||||
`goreleaser r --snapshot --clean`.
|
@ -1,4 +1,4 @@
|
||||
# Zig Builds
|
||||
# Builds (Zig)
|
||||
|
||||
<!-- md:version v2.5-unreleased -->
|
||||
|
||||
@ -35,7 +35,7 @@ builds:
|
||||
# This is the working directory for the Zig build command(s).
|
||||
#
|
||||
# Default: '.'.
|
||||
dir: go
|
||||
dir: my-app
|
||||
|
||||
# Set a specific zig binary to use when building.
|
||||
# It is safe to ignore this option in most cases.
|
||||
|
@ -112,6 +112,7 @@ nav:
|
||||
- Split & Merge: customization/partial.md
|
||||
- Build:
|
||||
- customization/builds.md
|
||||
- customization/rust-builds.md
|
||||
- customization/zig-builds.md
|
||||
- customization/prebuilt.md
|
||||
- customization/verifiable_builds.md
|
||||
|
Loading…
Reference in New Issue
Block a user