feat(build): add rust project builder

This commit is contained in:
Virgil 2026-04-01 11:14:23 +00:00
parent 91037a9443
commit b565752f9f
8 changed files with 359 additions and 3 deletions

View file

@ -392,6 +392,8 @@ func getBuilder(projectType build.ProjectType) (build.Builder, error) {
return builders.NewNodeBuilder(), nil
case build.ProjectTypePHP:
return builders.NewPHPBuilder(), nil
case build.ProjectTypeRust:
return builders.NewRustBuilder(), nil
case build.ProjectTypeDocs:
return builders.NewDocsBuilder(), nil
default:

View file

@ -56,6 +56,7 @@ type Artifact struct {
| **WailsBuilder** | `wails.json` | Checks `go.mod` for Wails v3 vs v2. V3 delegates to TaskfileBuilder; V2 runs `wails build -platform` then copies from `build/bin/` to `dist/`. |
| **NodeBuilder** | `package.json` | Detects the active package manager from lockfiles, runs the build script once per target, and collects artifacts from `dist/{os}_{arch}/`. |
| **PHPBuilder** | `composer.json` | Runs `composer install`, then `composer run-script build` when present. Falls back to a deterministic zip bundle in `dist/{os}_{arch}/`. |
| **RustBuilder** | `Cargo.toml` | Runs `cargo build --release --target` per platform and collects executables from `target/{triple}/release/`. |
| **DocsBuilder** | `mkdocs.yml` | Runs `mkdocs build --clean --site-dir` and packages the generated `site/` tree into a zip bundle per target. |
| **DockerBuilder** | `Dockerfile` | Validates `docker` and `buildx`, builds multi-platform images with `docker buildx build --platform`. Supports `--push` or local load/OCI tarball. |
| **LinuxKitBuilder** | `linuxkit.yml` or `.core/linuxkit/*.yml` | Validates `linuxkit` CLI, runs `linuxkit build --format --name --dir --arch`. Outputs qcow2, iso, raw, vmdk, vhd, or cloud images. Linux-only targets. |

View file

@ -7,7 +7,7 @@ description: Build system, release pipeline, and SDK generation for the Core eco
`forge.lthn.ai/core/go-build` is the build, release, and SDK generation toolkit for Core projects. It provides:
- **Auto-detecting builders** for Go, Wails, Node, PHP, Docs, Docker, LinuxKit, C++, and Taskfile projects
- **Auto-detecting builders** for Go, Wails, Node, PHP, Rust, Docs, Docker, LinuxKit, C++, and Taskfile projects
- **Cross-compilation** with per-target archiving (tar.gz, tar.xz, zip) and SHA-256 checksums
- **Code signing** -- macOS codesign with notarisation, GPG detached signatures, Windows signtool
- **Release automation** -- semantic versioning from git tags, conventional-commit changelogs, multi-target publishing
@ -43,6 +43,7 @@ The builder is chosen by marker-file priority:
| `go.mod` | Go |
| `package.json` | Node |
| `composer.json` | PHP |
| `Cargo.toml` | Rust |
| `mkdocs.yml` | Docs |
| `CMakeLists.txt` | C++ |
| `Dockerfile` | Docker |

View file

@ -1,6 +1,6 @@
// Package build provides project type detection and cross-compilation for the Core build system.
// It supports Go, Wails, Node.js, and PHP projects with automatic detection based on
// marker files (go.mod, wails.json, package.json, composer.json).
// It supports Go, Wails, Node.js, PHP, and Rust projects with automatic detection based on
// marker files (go.mod, wails.json, package.json, composer.json, Cargo.toml).
package build
import (

195
pkg/build/builders/rust.go Normal file
View file

@ -0,0 +1,195 @@
// Package builders provides build implementations for different project types.
package builders
import (
"context"
"runtime"
"dappco.re/go/core"
"dappco.re/go/core/build/internal/ax"
"dappco.re/go/core/build/pkg/build"
"dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
// RustBuilder implements the Builder interface for Rust projects.
//
// b := builders.NewRustBuilder()
type RustBuilder struct{}
// NewRustBuilder creates a new RustBuilder instance.
//
// b := builders.NewRustBuilder()
func NewRustBuilder() *RustBuilder {
return &RustBuilder{}
}
// Name returns the builder's identifier.
//
// name := b.Name() // → "rust"
func (b *RustBuilder) Name() string {
return "rust"
}
// Detect checks if this builder can handle the project in the given directory.
//
// ok, err := b.Detect(io.Local, ".")
func (b *RustBuilder) Detect(fs io.Medium, dir string) (bool, error) {
return build.IsRustProject(fs, dir), nil
}
// Build compiles the Rust project for the specified targets.
//
// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
func (b *RustBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
if cfg == nil {
return nil, coreerr.E("RustBuilder.Build", "config is nil", nil)
}
cargoCommand, err := b.resolveCargoCli()
if err != nil {
return nil, err
}
if len(targets) == 0 {
targets = []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
}
outputDir := cfg.OutputDir
if outputDir == "" {
outputDir = ax.Join(cfg.ProjectDir, "dist")
}
if err := cfg.FS.EnsureDir(outputDir); err != nil {
return nil, coreerr.E("RustBuilder.Build", "failed to create output directory", err)
}
var artifacts []build.Artifact
for _, target := range targets {
targetTriple, err := rustTargetTriple(target)
if err != nil {
return artifacts, err
}
platformDir := ax.Join(outputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
if err := cfg.FS.EnsureDir(platformDir); err != nil {
return artifacts, coreerr.E("RustBuilder.Build", "failed to create platform directory", err)
}
env := append([]string{}, cfg.Env...)
env = append(env,
core.Sprintf("CARGO_TARGET_DIR=%s", platformDir),
core.Sprintf("TARGET_OS=%s", target.OS),
core.Sprintf("TARGET_ARCH=%s", target.Arch),
)
if cfg.Name != "" {
env = append(env, core.Sprintf("NAME=%s", cfg.Name))
}
if cfg.Version != "" {
env = append(env, core.Sprintf("VERSION=%s", cfg.Version))
}
args := []string{"build", "--release", "--target", targetTriple}
output, err := ax.CombinedOutput(ctx, cfg.ProjectDir, env, cargoCommand, args...)
if err != nil {
return artifacts, coreerr.E("RustBuilder.Build", "cargo build failed: "+output, err)
}
found := b.findArtifactsForTarget(cfg.FS, platformDir, targetTriple, target)
if len(found) == 0 {
return artifacts, coreerr.E("RustBuilder.Build", "no build artifacts found for "+target.String(), nil)
}
artifacts = append(artifacts, found...)
}
return artifacts, nil
}
// resolveCargoCli returns the executable path for cargo.
//
// command, err := b.resolveCargoCli()
func (b *RustBuilder) resolveCargoCli(paths ...string) (string, error) {
if len(paths) == 0 {
paths = []string{
"/usr/local/bin/cargo",
"/opt/homebrew/bin/cargo",
}
}
command, err := ax.ResolveCommand("cargo", paths...)
if err != nil {
return "", coreerr.E("RustBuilder.resolveCargoCli", "cargo CLI not found. Install Rust from https://www.rust-lang.org/tools/install", err)
}
return command, nil
}
// findArtifactsForTarget looks for compiled binaries in the cargo target directory.
func (b *RustBuilder) findArtifactsForTarget(fs io.Medium, targetDir, targetTriple string, target build.Target) []build.Artifact {
releaseDir := ax.Join(targetDir, targetTriple, "release")
if !fs.IsDir(releaseDir) {
return nil
}
entries, err := fs.List(releaseDir)
if err != nil {
return nil
}
var artifacts []build.Artifact
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if core.HasPrefix(name, ".") ||
core.HasSuffix(name, ".d") ||
core.HasSuffix(name, ".rlib") ||
core.HasSuffix(name, ".rmeta") ||
core.HasSuffix(name, ".a") ||
core.HasSuffix(name, ".lib") ||
core.HasSuffix(name, ".pdb") {
continue
}
fullPath := ax.Join(releaseDir, name)
if target.OS != "windows" {
info, statErr := fs.Stat(fullPath)
if statErr != nil || info.Mode()&0o111 == 0 {
continue
}
}
artifacts = append(artifacts, build.Artifact{
Path: fullPath,
OS: target.OS,
Arch: target.Arch,
})
}
return artifacts
}
// rustTargetTriple maps a build target to a Rust target triple.
func rustTargetTriple(target build.Target) (string, error) {
switch target.OS + "/" + target.Arch {
case "linux/amd64":
return "x86_64-unknown-linux-gnu", nil
case "linux/arm64":
return "aarch64-unknown-linux-gnu", nil
case "darwin/amd64":
return "x86_64-apple-darwin", nil
case "darwin/arm64":
return "aarch64-apple-darwin", nil
case "windows/amd64":
return "x86_64-pc-windows-msvc", nil
case "windows/arm64":
return "aarch64-pc-windows-msvc", nil
default:
return "", coreerr.E("RustBuilder.rustTargetTriple", "unsupported Rust target: "+target.String(), nil)
}
}
// Ensure RustBuilder implements the Builder interface.
var _ build.Builder = (*RustBuilder)(nil)

View file

@ -0,0 +1,148 @@
package builders
import (
"context"
"os"
"strings"
"testing"
"dappco.re/go/core/build/internal/ax"
"dappco.re/go/core/build/pkg/build"
"dappco.re/go/core/io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupFakeRustToolchain(t *testing.T, binDir string) {
t.Helper()
script := `#!/bin/sh
set -eu
log_file="${RUST_BUILD_LOG_FILE:-}"
if [ -n "$log_file" ]; then
printf '%s\n' "$(basename "$0")" >> "$log_file"
printf '%s\n' "$@" >> "$log_file"
printf '%s\n' "CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-}" >> "$log_file"
printf '%s\n' "TARGET_OS=${TARGET_OS:-}" >> "$log_file"
printf '%s\n' "TARGET_ARCH=${TARGET_ARCH:-}" >> "$log_file"
fi
target_triple=""
prev=""
for arg in "$@"; do
if [ "$prev" = "--target" ]; then
target_triple="$arg"
prev=""
continue
fi
if [ "$arg" = "--target" ]; then
prev="--target"
fi
done
target_dir="${CARGO_TARGET_DIR:-target}"
release_dir="$target_dir/$target_triple/release"
mkdir -p "$release_dir"
name="${NAME:-rustapp}"
artifact="$release_dir/$name"
case "$target_triple" in
*-windows-*)
artifact="$artifact.exe"
;;
esac
printf 'fake rust artifact\n' > "$artifact"
chmod +x "$artifact" 2>/dev/null || true
`
require.NoError(t, ax.WriteFile(ax.Join(binDir, "cargo"), []byte(script), 0o755))
}
func setupRustTestProject(t *testing.T) string {
t.Helper()
dir := t.TempDir()
require.NoError(t, ax.WriteFile(ax.Join(dir, "Cargo.toml"), []byte("[package]\nname = \"testapp\"\nversion = \"0.1.0\""), 0o644))
require.NoError(t, ax.MkdirAll(ax.Join(dir, "src"), 0o755))
require.NoError(t, ax.WriteFile(ax.Join(dir, "src", "main.rs"), []byte("fn main() {}"), 0o644))
return dir
}
func TestRust_RustBuilderName_Good(t *testing.T) {
builder := NewRustBuilder()
assert.Equal(t, "rust", builder.Name())
}
func TestRust_RustBuilderDetect_Good(t *testing.T) {
fs := io.Local
t.Run("detects Cargo.toml projects", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, ax.WriteFile(ax.Join(dir, "Cargo.toml"), []byte("{}"), 0o644))
builder := NewRustBuilder()
detected, err := builder.Detect(fs, dir)
assert.NoError(t, err)
assert.True(t, detected)
})
t.Run("returns false for empty directory", func(t *testing.T) {
builder := NewRustBuilder()
detected, err := builder.Detect(fs, t.TempDir())
assert.NoError(t, err)
assert.False(t, detected)
})
}
func TestRust_RustBuilderBuild_Good(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
binDir := t.TempDir()
setupFakeRustToolchain(t, binDir)
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
projectDir := setupRustTestProject(t)
outputDir := t.TempDir()
logDir := t.TempDir()
logPath := ax.Join(logDir, "rust.log")
t.Setenv("RUST_BUILD_LOG_FILE", logPath)
builder := NewRustBuilder()
cfg := &build.Config{
FS: io.Local,
ProjectDir: projectDir,
OutputDir: outputDir,
Name: "testapp",
Version: "v1.2.3",
}
targets := []build.Target{{OS: "linux", Arch: "amd64"}}
artifacts, err := builder.Build(context.Background(), cfg, targets)
require.NoError(t, err)
require.Len(t, artifacts, 1)
assert.FileExists(t, artifacts[0].Path)
assert.Equal(t, "linux", artifacts[0].OS)
assert.Equal(t, "amd64", artifacts[0].Arch)
content, err := ax.ReadFile(logPath)
require.NoError(t, err)
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
require.GreaterOrEqual(t, len(lines), 5)
assert.Equal(t, "cargo", lines[0])
assert.Equal(t, "build", lines[1])
assert.Equal(t, "--release", lines[2])
assert.Equal(t, "--target", lines[3])
assert.Equal(t, "x86_64-unknown-linux-gnu", lines[4])
assert.Contains(t, lines, "CARGO_TARGET_DIR="+ax.Join(outputDir, "linux_amd64"))
}
func TestRust_RustBuilderInterface_Good(t *testing.T) {
var _ build.Builder = (*RustBuilder)(nil)
var _ build.Builder = NewRustBuilder()
}

View file

@ -331,6 +331,8 @@ func getBuilder(projectType build.ProjectType) (build.Builder, error) {
return builders.NewNodeBuilder(), nil
case build.ProjectTypePHP:
return builders.NewPHPBuilder(), nil
case build.ProjectTypeRust:
return builders.NewRustBuilder(), nil
case build.ProjectTypeDocs:
return builders.NewDocsBuilder(), nil
default:

View file

@ -185,6 +185,13 @@ func TestRelease_GetBuilder_Good(t *testing.T) {
assert.NotNil(t, builder)
assert.Equal(t, "php", builder.Name())
})
t.Run("returns Rust builder for rust project type", func(t *testing.T) {
builder, err := getBuilder(build.ProjectTypeRust)
require.NoError(t, err)
assert.NotNil(t, builder)
assert.Equal(t, "rust", builder.Name())
})
}
func TestRelease_GetBuilder_Bad(t *testing.T) {