Skip to content

Commit

Permalink
feat(build): rust support (#5325)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
caarlos0 and vedantmgoyal9 authored Dec 2, 2024
1 parent 6cea4fc commit d1b5110
Show file tree
Hide file tree
Showing 28 changed files with 886 additions and 30 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ manpages
output.json
.direnv
*.pyc
.intentionally-empty-file.o
2 changes: 1 addition & 1 deletion cmd/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 8 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand Down Expand Up @@ -88,7 +95,7 @@ func newInitCmd() *initCmd {
_ = cmd.RegisterFlagCompletionFunc(
"language",
cobra.FixedCompletions(
[]string{"go", "zig"},
[]string{"go", "rust", "zig"},
cobra.ShellCompDirectiveDefault,
),
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/release_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions internal/builders/rust/all_targets.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

262 changes: 262 additions & 0 deletions internal/builders/rust/build.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit d1b5110

Please sign in to comment.