1
0
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:
Carlos Alexandro Becker 2024-12-02 20:55:22 -03:00 committed by GitHub
parent 6cea4fc5b6
commit d1b5110615
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 886 additions and 30 deletions

3
.gitattributes vendored
View File

@ -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
View File

@ -16,3 +16,4 @@ manpages
output.json
.direnv
*.pyc
.intentionally-empty-file.o

View File

@ -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) {

View File

@ -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,
),
)

View File

@ -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
View File

@ -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
View 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

View 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
}

View 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)
})
}

View 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
}

View 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)
}

View 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",
}
}

View File

@ -0,0 +1 @@
target

1
internal/builders/rust/testdata/proj/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
target

7
internal/builders/rust/testdata/proj/Cargo.lock generated vendored Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View File

@ -0,0 +1,3 @@
[workspace]
members = ["bar", "foo"]
resolver = "2"

View File

@ -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

View File

@ -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)
}

View File

@ -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

View 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).

View File

@ -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
}

View File

@ -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"`

View File

@ -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

View 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`.

View File

@ -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.

View File

@ -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