diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..73fd180
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,80 @@
+name: CI
+
+on:
+ push:
+ branches: [dev, main]
+ pull_request:
+ branches: [dev, main]
+
+permissions:
+ contents: read
+
+env:
+ GOFLAGS: -buildvcs=false
+ GOWORK: "off"
+ GOPROXY: "direct"
+ GOSUMDB: "off"
+
+jobs:
+ test:
+ name: Test + Coverage
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-go@v6
+ with:
+ go-version: '1.26'
+ - name: Test with coverage
+ working-directory: go
+ run: go test -race -coverprofile=coverage.out -covermode=atomic -count=1 ./...
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: go/coverage.out
+ flags: unittests
+ fail_ci_if_error: false
+
+ lint:
+ name: golangci-lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-go@v6
+ with:
+ go-version: '1.26'
+ - uses: golangci/golangci-lint-action@v9
+ with:
+ version: latest
+ working-directory: go
+ args: --timeout=5m --tests=false
+
+ sonarcloud:
+ name: SonarCloud
+ runs-on: ubuntu-latest
+ needs: test
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-go@v6
+ with:
+ go-version: '1.26'
+ - name: Test for coverage
+ working-directory: go
+ run: go test -coverprofile=coverage.out -covermode=atomic -count=1 ./...
+ - name: SonarCloud Scan
+ uses: SonarSource/sonarqube-scan-action@v6
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ with:
+ args: >
+ -Dsonar.organization=dappcore
+ -Dsonar.projectKey=dappcore_go-build
+ -Dsonar.sources=go
+ -Dsonar.exclusions=**/vendor/**,**/third_party/**,**/.tmp/**,**/*_test.go
+ -Dsonar.tests=go
+ -Dsonar.test.inclusions=**/*_test.go
+ -Dsonar.go.coverage.reportPaths=go/coverage.out
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..f71254f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,4 @@
+[submodule "external/go"]
+ path = external/go
+ url = https://github.com/dappcore/go.git
+ branch = dev
diff --git a/.woodpecker.yml b/.woodpecker.yml
index 107f0e6..60358ee 100644
--- a/.woodpecker.yml
+++ b/.woodpecker.yml
@@ -14,7 +14,7 @@ steps:
GOFLAGS: -buildvcs=false
GOWORK: "off"
commands:
- - golangci-lint run --timeout=5m ./...
+ - cd go && golangci-lint run --timeout=5m ./...
- name: go-test
image: golang:1.26-alpine
@@ -25,7 +25,7 @@ steps:
CGO_ENABLED: "1"
commands:
- apk add --no-cache git build-base
- - go test -race -coverprofile=coverage.out -covermode=atomic -count=1 ./...
+ - cd go && go test -race -coverprofile=coverage.out -covermode=atomic -count=1 ./...
- name: sonar
image: sonarsource/sonar-scanner-cli:latest
depends_on: [go-test]
diff --git a/CLAUDE.md b/CLAUDE.md
index 9057a79..f870bb1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -24,6 +24,48 @@ go test ./pkg/build/... -run TestWorkflow_WriteReleaseWorkflow_Good
go test ./pkg/build/... -run TestApple_
```
+## Repo Layout
+
+```
+core/go-build/
+├── go/ ← Go module root (module dappco.re/go/build)
+│ ├── cmd/ ← CLI entry points
+│ ├── internal/ ← Go internal packages
+│ ├── pkg/ ← Go library packages
+│ ├── tests/ ← Go tests/fixtures
+│ │ └── cli/
+│ ├── go.mod
+│ ├── go.sum
+│ ├── CLAUDE.md ← symlink to root CLAUDE.md
+│ ├── README.md ← symlink to root README.md
+│ ├── AGENTS.md ← symlink to root AGENTS.md
+│ └── docs ← symlink to root docs/
+├── docs/ ← cross-language docs (symlinked into go/)
+├── locales/ ← locale content
+├── ui/ ← language-specific UI
+├── README.md
+├── CLAUDE.md
+├── AGENTS.md
+└── ...
+```
+
+Future language siblings are expected at repo root (`php/`, `ts/`, `py/`) while Go stays in `go/`.
+
+## Go Resolution Modes
+
+This repo is intentionally non-workspace: a single Go module under `go/`.
+
+| Mode | When | What runs |
+|------|------|-----------|
+| **Local module mode** | Standard local commands from repo root via `cd go` | Uses `go/ go.mod` and cached dependencies in module mode. |
+| **`GOWORK=off`** | CI and reproducible verification | Uses `go/` module graph directly, without workspace indirection. |
+
+```bash
+cd go
+go mod tidy
+GOWORK=off GOFLAGS=-mod=mod go test -count=1 -short ./...
+```
+
## Main Packages
- `pkg/build/`: discovery, config loading, caches, checksums, archives, workflow generation, Apple implementation
diff --git a/README.md b/README.md
index 2492dd0..973b561 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,21 @@
+
+
# go-build
+> Go build orchestrator — cmd, project detection, release publishers
+
+[](https://github.com/dappcore/go-build/actions/workflows/ci.yml)
+[](https://sonarcloud.io/dashboard?id=dappcore_go-build)
+[](https://codecov.io/gh/dappcore/go-build)
+[](https://sonarcloud.io/dashboard?id=dappcore_go-build)
+[](https://sonarcloud.io/dashboard?id=dappcore_go-build)
+[](https://sonarcloud.io/dashboard?id=dappcore_go-build)
+[](https://sonarcloud.io/dashboard?id=dappcore_go-build)
+[](https://sonarcloud.io/dashboard?id=dappcore_go-build)
+[](https://pkg.go.dev/dappco.re/go/go-build)
+[](https://eupl.eu/1.2/en/)
+
+
`dappco.re/go/build` is the build, release, SDK, Apple packaging, and workflow toolkit behind `core build`, `core release`, `core sdk`, `core ci`, and the public `dAppCore/build@v3` GitHub Action surface.
## What It Covers
diff --git a/cmd/build/ci_output.go b/cmd/build/ci_output.go
deleted file mode 100644
index 32c9bb3..0000000
--- a/cmd/build/ci_output.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package buildcmd
-
-import (
- "dappco.re/go"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/pkg/build"
-)
-
-func emitCIErrorAnnotation(result core.Result) {
- if result.OK {
- return
- }
-
- message := core.Trim(result.Error())
- if message == "" {
- return
- }
-
- cli.Print("%s\n", build.FormatGitHubAnnotation("error", "", 1, message))
-}
diff --git a/cmd/build/ci_output_test.go b/cmd/build/ci_output_test.go
deleted file mode 100644
index 7302fdc..0000000
--- a/cmd/build/ci_output_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package buildcmd
-
-import "dappco.re/go/build/pkg/build"
-
-func emitCIAnnotationForTest(err error) string {
- if err == nil {
- return ""
- }
- return build.FormatGitHubAnnotation("error", "", 1, err.Error())
-}
diff --git a/cmd/build/cmd_apple.go b/cmd/build/cmd_apple.go
deleted file mode 100644
index 087acbd..0000000
--- a/cmd/build/cmd_apple.go
+++ /dev/null
@@ -1,266 +0,0 @@
-package buildcmd
-
-import (
- "context"
- "regexp"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/internal/cmdutil"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-var buildAppleFn = build.BuildApple
-
-type appleCLIOptions struct {
- Arch string
- ArchChanged bool
- Sign bool
- SignChanged bool
- Notarise bool
- NotariseChanged bool
- DMG bool
- DMGChanged bool
- TestFlight bool
- TestFlightChanged bool
- AppStore bool
- AppStoreChanged bool
- TeamID string
- TeamIDChanged bool
- BundleID string
- BundleIDChanged bool
- Version string
- BuildNumber string
- ConfigPath string
- OutputDir string
-}
-
-// AddAppleCommand adds the Apple build subcommand to the build command.
-func AddAppleCommand(c *core.Core) {
- c.Command("build/apple", core.Command{
- Description: "cmd.build.apple.long",
- Action: func(opts core.Options) core.Result {
- return runAppleBuild(cmdutil.ContextOrBackground(), appleCLIOptions{
- Arch: cmdutil.OptionString(opts, "arch"),
- ArchChanged: opts.Has("arch"),
- Sign: cmdutil.OptionBoolDefault(opts, true, "sign"),
- SignChanged: opts.Has("sign"),
- Notarise: cmdutil.OptionBoolDefault(opts, true, "notarise"),
- NotariseChanged: opts.Has("notarise"),
- DMG: cmdutil.OptionBool(opts, "dmg"),
- DMGChanged: opts.Has("dmg"),
- TestFlight: cmdutil.OptionBool(opts, "testflight"),
- TestFlightChanged: opts.Has("testflight"),
- AppStore: cmdutil.OptionBool(opts, "appstore"),
- AppStoreChanged: opts.Has("appstore"),
- TeamID: cmdutil.OptionString(opts, "team-id"),
- TeamIDChanged: opts.Has("team-id"),
- BundleID: cmdutil.OptionString(opts, "bundle-id"),
- BundleIDChanged: opts.Has("bundle-id"),
- Version: cmdutil.OptionString(opts, "version"),
- BuildNumber: cmdutil.OptionString(opts, "build-number"),
- ConfigPath: cmdutil.OptionString(opts, "config"),
- OutputDir: cmdutil.OptionString(opts, "output"),
- })
- },
- })
-}
-
-func runAppleBuild(ctx context.Context, opts appleCLIOptions) core.Result {
- projectDirResult := ax.Getwd()
- if !projectDirResult.OK {
- return core.Fail(core.E("build.apple", "failed to get working directory", core.NewError(projectDirResult.Error())))
- }
- return runAppleBuildInDir(ctx, projectDirResult.Value.(string), opts)
-}
-
-func runAppleBuildInDir(ctx context.Context, projectDir string, opts appleCLIOptions) core.Result {
- if ctx == nil {
- ctx = context.Background()
- }
-
- filesystem := storage.Local
-
- buildConfigResult := loadAppleBuildConfig(filesystem, projectDir, opts.ConfigPath)
- if !buildConfigResult.OK {
- return buildConfigResult
- }
- buildConfig := buildConfigResult.Value.(*build.BuildConfig)
- cacheSetup := build.SetupBuildCache(filesystem, projectDir, buildConfig)
- if !cacheSetup.OK {
- return core.Fail(core.E("build.apple", "failed to set up build cache", core.NewError(cacheSetup.Error())))
- }
- if build.HasXcodeCloudConfig(buildConfig) {
- written := build.WriteXcodeCloudScripts(filesystem, projectDir, buildConfig)
- if !written.OK {
- return core.Fail(core.E("build.apple", "failed to write Xcode Cloud scripts", core.NewError(written.Error())))
- }
- }
-
- version := opts.Version
- if version == "" {
- versionResult := resolveBuildVersion(ctx, projectDir)
- if !versionResult.OK {
- return core.Fail(core.E("build.apple", "failed to determine version", core.NewError(versionResult.Error())))
- }
- version = versionResult.Value.(string)
- }
- validVersion := build.ValidateVersionIdentifier(version)
- if !validVersion.OK {
- return core.Fail(core.E("build.apple", "invalid build version; use a safe release identifier", core.NewError(validVersion.Error())))
- }
-
- buildNumber := opts.BuildNumber
- if buildNumber != "" {
- validBuildNumber := validateAppleBuildNumber(buildNumber)
- if !validBuildNumber.OK {
- return validBuildNumber
- }
- } else {
- buildNumberResult := resolveAppleBuildNumber(ctx, projectDir)
- if !buildNumberResult.OK {
- return buildNumberResult
- }
- buildNumber = buildNumberResult.Value.(string)
- }
-
- appleOptions := resolveAppleCommandOptions(buildConfig, opts)
-
- name := buildConfig.Project.Binary
- if name == "" {
- name = buildConfig.Project.Name
- }
- if name == "" {
- name = ax.Base(projectDir)
- }
-
- outputDir := opts.OutputDir
- if outputDir == "" {
- outputDir = ax.Join(projectDir, "dist", "apple")
- } else if !ax.IsAbs(outputDir) {
- outputDir = ax.Join(projectDir, outputDir)
- }
-
- runtimeCfg := buildRuntimeConfig(filesystem, projectDir, outputDir, name, buildConfig, false, "", version)
- resultValue := buildAppleFn(ctx, runtimeCfg, appleOptions, buildNumber)
- if !resultValue.OK {
- return resultValue
- }
- result := resultValue.Value.(*build.AppleBuildResult)
-
- cli.Print("%s %s\n", buildSuccessStyle.Render("Success"), "Apple build completed")
- cli.Print(" %s %s\n", "bundle", buildTargetStyle.Render(result.BundlePath))
- cli.Print(" %s %s\n", "version", buildTargetStyle.Render(result.Version))
- cli.Print(" %s %s\n", "build number", buildTargetStyle.Render(result.BuildNumber))
- if result.DMGPath != "" {
- cli.Print(" %s %s\n", "dmg", buildTargetStyle.Render(result.DMGPath))
- }
-
- return core.Ok(nil)
-}
-
-func loadAppleBuildConfig(filesystem storage.Medium, projectDir, configPath string) core.Result {
- if configPath == "" {
- cfg := build.LoadConfig(filesystem, projectDir)
- if !cfg.OK {
- return core.Fail(core.E("build.apple", "failed to load config", core.NewError(cfg.Error())))
- }
- return cfg
- }
-
- if !ax.IsAbs(configPath) {
- configPath = ax.Join(projectDir, configPath)
- }
- if !filesystem.Exists(configPath) {
- return core.Fail(core.E("build.apple", "build config not found: "+configPath, nil))
- }
-
- cfg := build.LoadConfigAtPath(filesystem, configPath)
- if !cfg.OK {
- return core.Fail(core.E("build.apple", "failed to load config", core.NewError(cfg.Error())))
- }
- return cfg
-}
-
-func resolveAppleCommandOptions(cfg *build.BuildConfig, overrides appleCLIOptions) build.AppleOptions {
- var options build.AppleOptions
- if cfg != nil {
- options = cfg.Apple.Resolve()
- options.CertIdentity = firstNonEmptyString(options.CertIdentity, cfg.Sign.MacOS.Identity)
- options.TeamID = firstNonEmptyString(options.TeamID, cfg.Sign.MacOS.TeamID)
- options.AppleID = firstNonEmptyString(options.AppleID, cfg.Sign.MacOS.AppleID)
- options.Password = firstNonEmptyString(options.Password, cfg.Sign.MacOS.AppPassword)
- } else {
- options = build.DefaultAppleOptions()
- }
-
- if overrides.ArchChanged {
- options.Arch = overrides.Arch
- }
- if overrides.SignChanged {
- options.Sign = overrides.Sign
- }
- if overrides.NotariseChanged {
- options.Notarise = overrides.Notarise
- }
- if overrides.DMGChanged {
- options.DMG = overrides.DMG
- }
- if overrides.TestFlightChanged {
- options.TestFlight = overrides.TestFlight
- }
- if overrides.AppStoreChanged {
- options.AppStore = overrides.AppStore
- }
- if overrides.TeamIDChanged {
- options.TeamID = overrides.TeamID
- }
- if overrides.BundleIDChanged {
- options.BundleID = overrides.BundleID
- }
-
- return options
-}
-
-func resolveAppleBuildNumber(ctx context.Context, projectDir string) core.Result {
- if value := core.Trim(core.Env("GITHUB_RUN_NUMBER")); value != "" {
- if validated := validateAppleBuildNumber(value); validated.OK {
- return core.Ok(value)
- }
- }
-
- outputResult := ax.RunDir(ctx, projectDir, "git", "rev-list", "--count", "HEAD")
- if !outputResult.OK {
- return core.Ok("1")
- }
-
- buildNumber := core.Trim(outputResult.Value.(string))
- if buildNumber == "" {
- return core.Ok("1")
- }
- validated := validateAppleBuildNumber(buildNumber)
- if !validated.OK {
- return validated
- }
- return core.Ok(buildNumber)
-}
-
-var appleBuildNumberPattern = regexp.MustCompile(`^[0-9]+$`)
-
-func validateAppleBuildNumber(value string) core.Result {
- if !appleBuildNumberPattern.MatchString(value) {
- return core.Fail(core.E("build.apple", "build-number must be a positive integer", nil))
- }
- return core.Ok(nil)
-}
-
-func firstNonEmptyString(values ...string) string {
- for _, value := range values {
- if core.Trim(value) != "" {
- return value
- }
- }
- return ""
-}
diff --git a/cmd/build/cmd_apple_example_test.go b/cmd/build/cmd_apple_example_test.go
deleted file mode 100644
index b88cb08..0000000
--- a/cmd/build/cmd_apple_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package buildcmd
-
-import core "dappco.re/go"
-
-// ExampleAddAppleCommand references AddAppleCommand on this package API surface.
-func ExampleAddAppleCommand() {
- _ = AddAppleCommand
- core.Println("AddAppleCommand")
- // Output: AddAppleCommand
-}
diff --git a/cmd/build/cmd_apple_test.go b/cmd/build/cmd_apple_test.go
deleted file mode 100644
index e3d71ed..0000000
--- a/cmd/build/cmd_apple_test.go
+++ /dev/null
@@ -1,369 +0,0 @@
-package buildcmd
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/testassert"
- "dappco.re/go/build/pkg/build"
- "dappco.re/go/build/pkg/build/signing"
-)
-
-func TestBuildCmd_resolveAppleCommandOptions_Good(t *testing.T) {
- cfg := &build.BuildConfig{
- Apple: build.AppleConfig{
- BundleID: "ai.lthn.core",
- Arch: "arm64",
- Sign: boolPtr(false),
- },
- Sign: signing.SignConfig{
- MacOS: signing.MacOSConfig{
- Identity: "Developer ID Application: Lethean CIC (ABC123DEF4)",
- TeamID: "ABC123DEF4",
- AppleID: "dev@example.com",
- AppPassword: "secret",
- },
- },
- }
-
- options := resolveAppleCommandOptions(cfg, appleCLIOptions{})
- if !stdlibAssertEqual("ai.lthn.core", options.BundleID) {
- t.Fatalf("want %v, got %v", "ai.lthn.core", options.BundleID)
- }
- if !stdlibAssertEqual("arm64", options.Arch) {
- t.Fatalf("want %v, got %v", "arm64", options.Arch)
- }
- if options.Sign {
- t.Fatal("expected false")
- }
- if !stdlibAssertEqual("Developer ID Application: Lethean CIC (ABC123DEF4)", options.CertIdentity) {
- t.Fatalf("want %v, got %v", "Developer ID Application: Lethean CIC (ABC123DEF4)", options.CertIdentity)
- }
- if !stdlibAssertEqual("ABC123DEF4", options.TeamID) {
- t.Fatalf("want %v, got %v", "ABC123DEF4", options.TeamID)
- }
- if !stdlibAssertEqual("dev@example.com", options.AppleID) {
- t.Fatalf("want %v, got %v", "dev@example.com", options.AppleID)
- }
- if !stdlibAssertEqual("secret", options.Password) {
- t.Fatalf("want %v, got %v", "secret", options.Password)
- }
-
- options = resolveAppleCommandOptions(cfg, appleCLIOptions{
- Arch: "universal",
- ArchChanged: true,
- Sign: true,
- SignChanged: true,
- BundleID: "ai.lthn.core.preview",
- BundleIDChanged: true,
- TeamID: "ZZZ9876543",
- TeamIDChanged: true,
- TestFlight: true,
- TestFlightChanged: true,
- })
- if !stdlibAssertEqual("universal", options.Arch) {
- t.Fatalf("want %v, got %v", "universal", options.Arch)
- }
- if !(options.Sign) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("ai.lthn.core.preview", options.BundleID) {
- t.Fatalf("want %v, got %v", "ai.lthn.core.preview", options.BundleID)
- }
- if !stdlibAssertEqual("ZZZ9876543", options.TeamID) {
- t.Fatalf("want %v, got %v", "ZZZ9876543", options.TeamID)
- }
- if !(options.TestFlight) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestBuildCmd_resolveAppleBuildNumber_Good(t *testing.T) {
- t.Run("prefers github run number when valid", func(t *testing.T) {
- t.Setenv("GITHUB_RUN_NUMBER", "77")
- value := requireBuildCmdString(t, resolveAppleBuildNumber(context.Background(), t.TempDir()))
- if !stdlibAssertEqual("77", value) {
- t.Fatalf("want %v, got %v", "77", value)
- }
-
- })
-
- t.Run("falls back to git commit count", func(t *testing.T) {
- dir := t.TempDir()
- runGit(t, dir, "init")
- runGit(t, dir, "config", "user.email", "test@example.com")
- runGit(t, dir, "config", "user.name", "Test User")
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(dir, "README.md"), []byte("hello\n"), 0o644))
-
- runGit(t, dir, "add", ".")
- runGit(t, dir, "commit", "-m", "feat: initial commit")
-
- t.Setenv("GITHUB_RUN_NUMBER", "")
- value := requireBuildCmdString(t, resolveAppleBuildNumber(context.Background(), dir))
- if !stdlibAssertEqual("1", value) {
- t.Fatalf("want %v, got %v", "1", value)
- }
-
- })
-}
-
-func TestBuildCmd_AddAppleCommand_Good(t *testing.T) {
- c := core.New()
- AddAppleCommand(c)
-
- result := c.Command("build/apple")
- if !(result.OK) {
- t.Fatal("expected true")
- }
-
- command, ok := result.Value.(*core.Command)
- if !(ok) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("build/apple", command.Path) {
- t.Fatalf("want %v, got %v", "build/apple", command.Path)
- }
- if !stdlibAssertEqual("cmd.build.apple.long", command.Description) {
- t.Fatalf("want %v, got %v", "cmd.build.apple.long", command.Description)
- }
-
-}
-
-func TestBuildCmd_runAppleBuildInDir_Good(t *testing.T) {
- projectDir := t.TempDir()
- coreDir := ax.Join(projectDir, ".core")
- requireBuildCmdOK(t, ax.MkdirAll(coreDir, 0o755))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(coreDir, "build.yaml"), []byte(`
-project:
- name: Core
- binary: Core
-apple:
- bundle_id: ai.lthn.core
- sign: false
-sign:
- macos:
- identity: "Developer ID Application: Lethean CIC (ABC123DEF4)"
- team_id: ABC123DEF4
- apple_id: dev@example.com
- app_password: secret
-`), 0o644))
-
- oldBuildApple := buildAppleFn
- t.Cleanup(func() {
- buildAppleFn = oldBuildApple
- })
-
- var called bool
- buildAppleFn = func(ctx context.Context, cfg *build.Config, options build.AppleOptions, buildNumber string) core.Result {
- called = true
- if !stdlibAssertEqual(ax.Join(projectDir, "out"), cfg.OutputDir) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "out"), cfg.OutputDir)
- }
- if !stdlibAssertEqual("Core", cfg.Name) {
- t.Fatalf("want %v, got %v", "Core", cfg.Name)
- }
- if !stdlibAssertEqual("v1.2.3", cfg.Version) {
- t.Fatalf("want %v, got %v", "v1.2.3", cfg.Version)
- }
- if !stdlibAssertEqual("42", buildNumber) {
- t.Fatalf("want %v, got %v", "42", buildNumber)
- }
- if !stdlibAssertEqual("ai.lthn.core", options.BundleID) {
- t.Fatalf("want %v, got %v", "ai.lthn.core", options.BundleID)
- }
- if !(options.Sign) {
- t.Fatal("expected true")
- }
-
- return core.Ok(&build.AppleBuildResult{
- BundlePath: ax.Join(cfg.OutputDir, "Core.app"),
- Version: "1.2.3",
- BuildNumber: buildNumber,
- })
- }
-
- requireBuildCmdOK(t, runAppleBuildInDir(context.Background(), projectDir, appleCLIOptions{
- Sign: true,
- SignChanged: true,
- Version: "v1.2.3",
- BuildNumber: "42",
- OutputDir: "out",
- }))
- if !(called) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestBuildCmd_runAppleBuildInDir_RejectsUnsafeVersion_Bad(t *testing.T) {
- projectDir := t.TempDir()
- coreDir := ax.Join(projectDir, ".core")
- requireBuildCmdOK(t, ax.MkdirAll(coreDir, 0o755))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(coreDir, "build.yaml"), []byte(`
-project:
- name: Core
- binary: Core
-apple:
- bundle_id: ai.lthn.core
- sign: false
-`), 0o644))
-
- oldBuildApple := buildAppleFn
- t.Cleanup(func() {
- buildAppleFn = oldBuildApple
- })
-
- buildAppleFn = func(ctx context.Context, cfg *build.Config, options build.AppleOptions, buildNumber string) core.Result {
- t.Fatal("buildAppleFn must not be called for unsafe versions")
- return core.Ok(nil)
- }
-
- message := requireBuildCmdError(t, runAppleBuildInDir(context.Background(), projectDir, appleCLIOptions{
- Version: "v1.2.3 --bad",
- BuildNumber: "42",
- }))
- if !stdlibAssertContains(message, "invalid build version") {
- t.Fatalf("expected %v to contain %v", message, "invalid build version")
- }
-
-}
-
-func TestBuildCmd_runAppleBuildInDir_SetsUpBuildCache_Good(t *testing.T) {
- projectDir := t.TempDir()
- coreDir := ax.Join(projectDir, ".core")
- requireBuildCmdOK(t, ax.MkdirAll(coreDir, 0o755))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(coreDir, "build.yaml"), []byte(`
-project:
- name: Core
- binary: Core
-build:
- cache:
- enabled: true
- paths:
- - cache/go-build
- - cache/go-mod
-apple:
- bundle_id: ai.lthn.core
- sign: false
-`), 0o644))
-
- oldBuildApple := buildAppleFn
- t.Cleanup(func() {
- buildAppleFn = oldBuildApple
- })
-
- buildAppleFn = func(ctx context.Context, cfg *build.Config, options build.AppleOptions, buildNumber string) core.Result {
- if !stdlibAssertEqual([]string{ax.Join(projectDir, "cache", "go-build"), ax.Join(projectDir, "cache", "go-mod")}, cfg.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{ax.Join(projectDir, "cache", "go-build"), ax.Join(projectDir, "cache", "go-mod")}, cfg.Cache.Paths)
- }
- if !(cfg.Cache.Enabled) {
- t.Fatal("expected true")
- }
- if !(cfg.FS.Exists(ax.Join(projectDir, ".core", "cache"))) {
- t.Fatal("expected true")
- }
- if !(cfg.FS.Exists(ax.Join(projectDir, "cache", "go-build"))) {
- t.Fatal("expected true")
- }
- if !(cfg.FS.Exists(ax.Join(projectDir, "cache", "go-mod"))) {
- t.Fatal("expected true")
- }
-
- return core.Ok(&build.AppleBuildResult{
- BundlePath: ax.Join(cfg.OutputDir, "Core.app"),
- Version: "1.2.3",
- BuildNumber: buildNumber,
- })
- }
-
- requireBuildCmdOK(t, runAppleBuildInDir(context.Background(), projectDir, appleCLIOptions{
- Version: "v1.2.3",
- BuildNumber: "42",
- }))
-
-}
-
-func TestBuildCmd_runAppleBuildInDir_WritesXcodeCloudScripts_Good(t *testing.T) {
- projectDir := t.TempDir()
- coreDir := ax.Join(projectDir, ".core")
- requireBuildCmdOK(t, ax.MkdirAll(coreDir, 0o755))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(coreDir, "build.yaml"), []byte(`
-project:
- name: Core
- binary: Core
-apple:
- bundle_id: ai.lthn.core
- sign: false
- xcode_cloud:
- workflow: CoreGUI Release
-`), 0o644))
-
- oldBuildApple := buildAppleFn
- t.Cleanup(func() {
- buildAppleFn = oldBuildApple
- })
-
- buildAppleFn = func(ctx context.Context, cfg *build.Config, options build.AppleOptions, buildNumber string) core.Result {
- return core.Ok(&build.AppleBuildResult{
- BundlePath: ax.Join(cfg.OutputDir, "Core.app"),
- Version: "1.2.3",
- BuildNumber: buildNumber,
- })
- }
-
- requireBuildCmdOK(t, runAppleBuildInDir(context.Background(), projectDir, appleCLIOptions{
- Version: "v1.2.3",
- BuildNumber: "42",
- }))
-
- preScriptPath := ax.Join(projectDir, build.XcodeCloudScriptsDir, build.XcodeCloudPreXcodebuildScriptName)
- preScript := requireBuildCmdBytes(t, ax.ReadFile(preScriptPath))
- if !stdlibAssertContains(string(preScript), `core build apple --arch 'universal' --config '.core/build.yaml'`) {
- t.Fatalf("expected %v to contain %v", string(preScript), `core build apple --arch 'universal' --config '.core/build.yaml'`)
- }
-
-}
-
-func boolPtr(value bool) *bool {
- return &value
-}
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCmdApple_AddAppleCommand_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- AddAppleCommand(core.New())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCmdApple_AddAppleCommand_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- AddAppleCommand(core.New())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCmdApple_AddAppleCommand_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- AddAppleCommand(core.New())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/cmd/build/cmd_build.go b/cmd/build/cmd_build.go
deleted file mode 100644
index 4e4bb19..0000000
--- a/cmd/build/cmd_build.go
+++ /dev/null
@@ -1,165 +0,0 @@
-// Package buildcmd registers auto-detected project build commands.
-package buildcmd
-
-import (
- "embed"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/internal/cmdutil"
- _ "dappco.re/go/build/locales" // registers locale translations
-)
-
-// Style aliases used by build command output.
-var (
- buildHeaderStyle = cli.TitleStyle
- buildTargetStyle = cli.ValueStyle
- buildSuccessStyle = cli.SuccessStyle
- buildErrorStyle = cli.ErrorStyle
- buildDimStyle = cli.DimStyle
-)
-
-//go:embed all:tmpl/gui
-var guiTemplate embed.FS
-
-const buildPathOptionKey = "pa" + "th"
-
-// AddBuildCommands registers the 'build' command and all subcommands.
-//
-// buildcmd.AddBuildCommands(root)
-func AddBuildCommands(c *core.Core) {
- c.Command("build", core.Command{
- Description: "cmd.build.long",
- Action: func(opts core.Options) core.Result {
- archiveOutput := cmdutil.OptionBoolDefault(opts, false, "archive")
- archiveOutputSet := cmdutil.OptionHas(opts, "archive")
- checksumOutput := cmdutil.OptionBoolDefault(opts, false, "checksum")
- checksumOutputSet := cmdutil.OptionHas(opts, "checksum")
- packageEnabled := cmdutil.OptionBoolDefault(opts, false, "package")
- packageSet := cmdutil.OptionHas(opts, "package")
- archiveOutput, checksumOutput = resolvePackageOutputs(
- packageEnabled,
- packageSet,
- archiveOutput,
- archiveOutputSet,
- checksumOutput,
- checksumOutputSet,
- )
-
- return runProjectBuild(ProjectBuildRequest{
- Context: cmdutil.ContextOrBackground(),
- BuildType: cmdutil.OptionString(opts, "type"),
- Version: cmdutil.OptionString(opts, "version"),
- CIMode: cmdutil.OptionBool(opts, "ci"),
- TargetsFlag: cmdutil.OptionString(opts, "targets", "build-platform", "build_platform"),
- OutputDir: cmdutil.OptionString(opts, "output"),
- BuildName: cmdutil.OptionString(opts, "name", "build-name", "build_name"),
- BuildTagsFlag: cmdutil.OptionString(opts, "build-tags", "build_tags"),
- Obfuscate: cmdutil.OptionBool(opts, "build-obfuscate", "build_obfuscate", "obfuscate"),
- ObfuscateSet: cmdutil.OptionHas(opts, "build-obfuscate", "build_obfuscate", "obfuscate"),
- NSIS: cmdutil.OptionBool(opts, "nsis"),
- NSISSet: cmdutil.OptionHas(opts, "nsis"),
- WebView2: cmdutil.OptionString(opts, "wails-build-webview2", "wails_build_webview2", "webview2"),
- WebView2Set: cmdutil.OptionHas(opts, "wails-build-webview2", "wails_build_webview2", "webview2"),
- DenoBuild: cmdutil.OptionString(opts, "deno-build", "deno_build"),
- DenoBuildSet: cmdutil.OptionHas(opts, "deno-build", "deno_build"),
- NpmBuild: cmdutil.OptionString(opts, "npm-build", "npm_build"),
- NpmBuildSet: cmdutil.OptionHas(opts, "npm-build", "npm_build"),
- BuildCache: cmdutil.OptionBool(opts, "build-cache", "build_cache"),
- BuildCacheSet: cmdutil.OptionHas(opts, "build-cache", "build_cache"),
- ArchiveOutput: archiveOutput,
- ArchiveOutputSet: archiveOutputSet,
- ChecksumOutput: checksumOutput,
- ChecksumOutputSet: checksumOutputSet,
- PackageSet: packageSet,
- ArchiveFormat: cmdutil.OptionString(opts, "archive-format"),
- ConfigPath: cmdutil.OptionString(opts, "config"),
- Format: cmdutil.OptionString(opts, "format"),
- Push: cmdutil.OptionBool(opts, "push"),
- ImageName: cmdutil.OptionString(opts, "image"),
- Sign: cmdutil.OptionBoolDefault(opts, true, "sign"),
- SignSet: cmdutil.OptionHas(opts, "sign"),
- NoSign: resolveNoSign(
- cmdutil.OptionBool(opts, "no-sign"),
- cmdutil.OptionBoolDefault(opts, true, "sign"),
- cmdutil.OptionHas(opts, "sign"),
- ),
- Notarize: cmdutil.OptionBool(opts, "notarize"),
- Verbose: cmdutil.OptionBool(opts, "verbose", "v"),
- })
- },
- })
-
- c.Command("build/from-path", core.Command{
- Description: "cmd.build.from_path.short",
- Action: func(opts core.Options) core.Result {
- fromPath := cmdutil.OptionString(opts, buildPathOptionKey)
- if fromPath == "" {
- return core.Fail(errPathRequired)
- }
- return runBuild(cmdutil.ContextOrBackground(), fromPath)
- },
- })
-
- c.Command("build/pwa", core.Command{
- Description: "cmd.build.pwa.short",
- Action: func(opts core.Options) core.Result {
- pwaPath := cmdutil.OptionString(opts, buildPathOptionKey)
- pwaURL := cmdutil.OptionString(opts, "url")
- switch {
- case pwaPath != "":
- return runLocalPwaBuild(cmdutil.ContextOrBackground(), pwaPath)
- case pwaURL != "":
- return runPwaBuild(cmdutil.ContextOrBackground(), pwaURL)
- default:
- return core.Fail(errPWAInputRequired)
- }
- },
- })
-
- c.Command("build/sdk", core.Command{
- Description: "cmd.build.sdk.long",
- Action: func(opts core.Options) core.Result {
- return runBuildSDK(
- cmdutil.ContextOrBackground(),
- cmdutil.OptionString(opts, "spec"),
- cmdutil.OptionString(opts, "lang"),
- cmdutil.OptionString(opts, "version"),
- cmdutil.OptionBool(opts, "dry-run"),
- cmdutil.OptionBool(opts, "skip-unavailable", "skip_unavailable"),
- )
- },
- })
-
- AddAppleCommand(c)
- AddImageCommand(c)
- AddInstallersCommand(c)
- AddReleaseCommand(c)
- AddServiceCommands(c)
- AddWorkflowCommand(c)
-}
-
-func resolveNoSign(noSign bool, signEnabled bool, signSet bool) bool {
- if noSign {
- return true
- }
- if signSet && !signEnabled {
- return true
- }
- return false
-}
-
-func resolvePackageOutputs(packageEnabled bool, packageSet bool, archiveOutput bool, archiveOutputSet bool, checksumOutput bool, checksumOutputSet bool) (bool, bool) {
- if !packageSet {
- return archiveOutput, checksumOutput
- }
-
- if !archiveOutputSet {
- archiveOutput = packageEnabled
- }
- if !checksumOutputSet {
- checksumOutput = packageEnabled
- }
-
- return archiveOutput, checksumOutput
-}
diff --git a/cmd/build/cmd_build_example_test.go b/cmd/build/cmd_build_example_test.go
deleted file mode 100644
index 2f6997c..0000000
--- a/cmd/build/cmd_build_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package buildcmd
-
-import core "dappco.re/go"
-
-// ExampleAddBuildCommands references AddBuildCommands on this package API surface.
-func ExampleAddBuildCommands() {
- _ = AddBuildCommands
- core.Println("AddBuildCommands")
- // Output: AddBuildCommands
-}
diff --git a/cmd/build/cmd_build_test.go b/cmd/build/cmd_build_test.go
deleted file mode 100644
index aed53a5..0000000
--- a/cmd/build/cmd_build_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package buildcmd
-
-import core "dappco.re/go"
-
-func TestCmdBuild_AddBuildCommands_Good(t *core.T) {
- c := core.New()
- AddBuildCommands(c)
- core.AssertNotNil(t, c)
-}
-
-func TestCmdBuild_AddBuildCommands_Bad(t *core.T) {
- c := core.New()
- core.AssertNotPanics(t, func() {
- AddBuildCommands(c)
- })
- core.AssertNotNil(t, c)
-}
-
-func TestCmdBuild_AddBuildCommands_Ugly(t *core.T) {
- c := core.New()
- AddBuildCommands(c)
- AddBuildCommands(core.New())
- core.AssertNotNil(t, c)
-}
diff --git a/cmd/build/cmd_commands.go b/cmd/build/cmd_commands.go
deleted file mode 100644
index 6d364c2..0000000
--- a/cmd/build/cmd_commands.go
+++ /dev/null
@@ -1,5 +0,0 @@
-// Package buildcmd registers build-oriented Core commands.
-//
-// buildcmd.AddBuildCommands(root)
-// buildcmd.AddReleaseCommand(buildCmd)
-package buildcmd
diff --git a/cmd/build/cmd_image.go b/cmd/build/cmd_image.go
deleted file mode 100644
index 958f938..0000000
--- a/cmd/build/cmd_image.go
+++ /dev/null
@@ -1,584 +0,0 @@
-package buildcmd
-
-import (
- "context"
- "io/fs" // AX-6: fs.FileMode is structural for core/io.Medium.WriteMode.
- "slices"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/internal/cmdutil"
- "dappco.re/go/build/pkg/build"
- "dappco.re/go/build/pkg/build/builders"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-type immutableImageVersion struct {
- BuildVersion string
- RetainVersion string
- CacheVersion string
-}
-
-// ImageBuildRequest groups the inputs for `core build image`.
-type ImageBuildRequest struct {
- Context context.Context
- Base string
- Format string
- OutputDir string
- List bool
- Rebuild bool
-}
-
-type imageBuildCacheMetadata struct {
- ImageName string `json:"image_name"`
- Base string `json:"base"`
- BaseVersion string `json:"base_version,omitempty"`
- BuildVersion string `json:"build_version"`
- Formats []string `json:"formats,omitempty"`
- Packages []string `json:"packages,omitempty"`
- Mounts []string `json:"mounts,omitempty"`
- GPU bool `json:"gpu,omitempty"`
- Registry string `json:"registry,omitempty"`
- Signature string `json:"signature"`
-}
-
-// AddImageCommand registers the immutable LinuxKit image builder command.
-func AddImageCommand(c *core.Core) {
- c.Command("build/image", core.Command{
- Description: "Build immutable LinuxKit base images",
- Action: func(opts core.Options) core.Result {
- return runBuildImage(ImageBuildRequest{
- Context: cmdutil.ContextOrBackground(),
- Base: resolveImageBase(opts),
- Format: cmdutil.OptionString(opts, "format"),
- OutputDir: cmdutil.OptionString(opts, "output"),
- List: cmdutil.OptionBool(opts, "list"),
- Rebuild: cmdutil.OptionBool(opts, "rebuild"),
- })
- },
- })
-}
-
-func resolveImageBase(opts core.Options) string {
- if base := cmdutil.OptionString(opts, "base", "name"); base != "" {
- return base
- }
- return opts.String("_arg")
-}
-
-// runBuildImage renders the embedded immutable LinuxKit image template and builds the requested formats.
-func runBuildImage(req ImageBuildRequest) core.Result {
- ctx := req.Context
- if ctx == nil {
- ctx = context.Background()
- }
-
- projectDirResult := ax.Getwd()
- if !projectDirResult.OK {
- return core.Fail(core.E("build.runBuildImage", "failed to get working directory", core.NewError(projectDirResult.Error())))
- }
- projectDir := projectDirResult.Value.(string)
-
- imageBuilder := builders.NewLinuxKitImageBuilder()
- if req.List {
- cli.Print("%s %s\n", buildHeaderStyle.Render("Images"), "available immutable LinuxKit bases")
- for _, baseImage := range imageBuilder.ListBaseImages() {
- cli.Print(" %s %s %s\n", buildTargetStyle.Render(baseImage.Name), buildDimStyle.Render(baseImage.Version), baseImage.Description)
- }
- return core.Ok(nil)
- }
-
- buildConfigResult := build.LoadConfig(coreio.Local, projectDir)
- if !buildConfigResult.OK {
- return core.Fail(core.E("build.runBuildImage", "failed to load build config", core.NewError(buildConfigResult.Error())))
- }
- buildConfig := buildConfigResult.Value.(*build.BuildConfig)
-
- if req.Base != "" {
- buildConfig.LinuxKit.Base = req.Base
- }
- if req.Format != "" {
- buildConfig.LinuxKit.Formats = parseImageFormats(req.Format)
- }
-
- outputDir := req.OutputDir
- if outputDir == "" {
- outputDir = "dist"
- }
- if !ax.IsAbs(outputDir) {
- outputDir = ax.Join(projectDir, outputDir)
- }
-
- versionInfo := resolveImmutableImageVersion(ctx, projectDir)
- version := versionInfo.BuildVersion
- validVersion := build.ValidateVersionIdentifier(version)
- if !validVersion.OK {
- return core.Fail(core.E("build.runBuildImage", "unsafe release tag detected for immutable image", core.NewError(validVersion.Error())))
- }
-
- imageName := buildConfig.LinuxKit.Base
- if imageName == "" {
- imageName = build.DefaultLinuxKitConfig().Base
- }
-
- runtimeCfg := &build.Config{
- FS: coreio.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: imageName,
- Version: version,
- LinuxKit: buildConfig.LinuxKit,
- }
-
- formats := runtimeCfg.LinuxKit.Formats
- if len(formats) == 0 {
- formats = append([]string(nil), build.DefaultLinuxKitConfig().Formats...)
- }
-
- cacheCfg := runtimeCfg.LinuxKit
- cacheCfg.Formats = append([]string(nil), formats...)
-
- artifacts := cachedImageArtifacts(imageBuilder, outputDir, imageName, formats)
- usedCache := !req.Rebuild && allImageArtifactsExist(coreio.Local, imageBuilder, outputDir, imageName, cacheCfg, versionInfo.CacheVersion)
- if usedCache {
- cli.Print("%s %s\n", buildSuccessStyle.Render("Using"), "cached immutable image artifacts")
- } else {
- built := imageBuilder.Build(ctx, runtimeCfg)
- if !built.OK {
- return built
- }
- artifacts = built.Value.([]build.Artifact)
- written := writeImageBuildCacheMetadata(coreio.Local, outputDir, imageName, cacheCfg, versionInfo.CacheVersion)
- if !written.OK {
- return core.Fail(core.E("build.runBuildImage", "failed to write image cache metadata", core.NewError(written.Error())))
- }
- }
-
- versionedArtifactsResult := retainVersionedImageArtifacts(coreio.Local, artifacts, versionInfo.RetainVersion)
- if !versionedArtifactsResult.OK {
- return core.Fail(core.E("build.runBuildImage", "failed to retain versioned immutable image artifacts", core.NewError(versionedArtifactsResult.Error())))
- }
- versionedArtifacts := versionedArtifactsResult.Value.([]string)
-
- publishedRef := ""
- if containsImageFormat(formats, "oci") && core.Trim(runtimeCfg.LinuxKit.Registry) != "" {
- ociArtifactPath := imageBuilder.ArtifactPath(outputDir, imageName, "oci")
- published := publishOCIImageArchive(ctx, projectDir, ociArtifactPath, runtimeCfg.LinuxKit.Registry, imageName, version)
- if !published.OK {
- return published
- }
- publishedRef = published.Value.(string)
- }
-
- if !usedCache {
- cli.Print("%s %s\n", buildSuccessStyle.Render("Built"), buildTargetStyle.Render(imageName))
- }
- for _, artifact := range artifacts {
- relPathResult := ax.Rel(projectDir, artifact.Path)
- relPath := artifact.Path
- if relPathResult.OK {
- relPath = relPathResult.Value.(string)
- } else {
- relPath = artifact.Path
- }
- cli.Print(" %s\n", relPath)
- }
- for _, artifactPath := range versionedArtifacts {
- relPathResult := ax.Rel(projectDir, artifactPath)
- relPath := artifactPath
- if relPathResult.OK {
- relPath = relPathResult.Value.(string)
- } else {
- relPath = artifactPath
- }
- cli.Print(" %s\n", relPath)
- }
- if publishedRef != "" {
- cli.Print("%s %s\n", buildSuccessStyle.Render("Published"), buildTargetStyle.Render(publishedRef))
- }
-
- return core.Ok(nil)
-}
-
-func resolveImmutableImageVersion(ctx context.Context, projectDir string) immutableImageVersion {
- if ctx == nil {
- ctx = context.Background()
- }
-
- if git := ax.LookPath("git"); !git.OK {
- return immutableImageVersion{BuildVersion: "dev"}
- }
-
- tagResult := ax.RunDir(ctx, projectDir, "git", "describe", "--tags", "--exact-match", "HEAD")
- if !tagResult.OK {
- return immutableImageVersion{BuildVersion: "dev"}
- }
-
- tag := core.Trim(tagResult.Value.(string))
- if tag == "" {
- return immutableImageVersion{BuildVersion: "dev"}
- }
- if !core.HasPrefix(tag, "v") {
- tag = "v" + tag
- }
-
- return immutableImageVersion{
- BuildVersion: tag,
- RetainVersion: tag,
- CacheVersion: tag,
- }
-}
-
-func parseImageFormats(value string) []string {
- if value == "" {
- return nil
- }
-
- parts := core.Split(value, ",")
- formats := make([]string, 0, len(parts))
- seen := make(map[string]struct{}, len(parts))
- for _, part := range parts {
- part = core.Lower(core.Trim(part))
- if part == "" {
- continue
- }
- if _, ok := seen[part]; ok {
- continue
- }
- seen[part] = struct{}{}
- formats = append(formats, part)
- }
- return formats
-}
-
-func cachedImageArtifacts(imageBuilder *builders.LinuxKitImageBuilder, outputDir, imageName string, formats []string) []build.Artifact {
- artifacts := make([]build.Artifact, 0, len(formats))
- for _, format := range formats {
- format = core.Trim(format)
- if format == "" {
- continue
- }
- artifacts = append(artifacts, build.Artifact{
- Path: imageBuilder.ArtifactPath(outputDir, imageName, format),
- OS: "linux",
- Arch: core.Env("ARCH"),
- })
- }
- return artifacts
-}
-
-func containsImageFormat(formats []string, want string) bool {
- want = core.Lower(core.Trim(want))
- for _, format := range formats {
- if core.Lower(core.Trim(format)) == want {
- return true
- }
- }
- return false
-}
-
-func retainVersionedImageArtifacts(filesystem coreio.Medium, artifacts []build.Artifact, version string) core.Result {
- versionTag := normalizeImageVersionTag(version)
- if versionTag == "" {
- return core.Ok([]string(nil))
- }
-
- versionedPaths := make([]string, 0, len(artifacts))
- for _, artifact := range artifacts {
- if artifact.Path == "" {
- continue
- }
- versionedPath := versionedImageArtifactPath(artifact.Path, versionTag)
- if versionedPath == artifact.Path {
- continue
- }
- copied := copyImageArtifact(filesystem, artifact.Path, versionedPath)
- if !copied.OK {
- return copied
- }
- versionedPaths = append(versionedPaths, versionedPath)
- }
-
- return core.Ok(versionedPaths)
-}
-
-func versionedImageArtifactPath(path, versionTag string) string {
- if path == "" || versionTag == "" {
- return path
- }
-
- ext := ax.Ext(path)
- base := core.TrimSuffix(ax.Base(path), ext)
- return ax.Join(ax.Dir(path), base+"-"+versionTag+ext)
-}
-
-func normalizeImageVersionTag(version string) string {
- version = core.Trim(version)
- version = core.TrimPrefix(version, "v")
- if version == "" {
- return ""
- }
-
- version = core.Replace(version, "/", "-")
- version = core.Replace(version, "\\", "-")
- version = core.Replace(version, ":", "-")
- version = core.Replace(version, " ", "-")
- version = core.Replace(version, "\t", "-")
- return trimImageVersionTagEdges(version)
-}
-
-func trimImageVersionTagEdges(version string) string {
- start := 0
- for start < len(version) && isImageVersionTagEdge(version[start]) {
- start++
- }
-
- end := len(version)
- for end > start && isImageVersionTagEdge(version[end-1]) {
- end--
- }
-
- return version[start:end]
-}
-
-func isImageVersionTagEdge(ch byte) bool {
- return ch == '-' || ch == '.'
-}
-
-func copyImageArtifact(filesystem coreio.Medium, sourcePath, destinationPath string) core.Result {
- content := filesystem.Read(sourcePath)
- if !content.OK {
- return content
- }
-
- mode := fs.FileMode(0o644)
- if info := filesystem.Stat(sourcePath); info.OK {
- mode = info.Value.(fs.FileInfo).Mode()
- }
-
- return filesystem.WriteMode(destinationPath, content.Value.(string), mode)
-}
-
-func publishOCIImageArchive(ctx context.Context, projectDir, artifactPath, registry, imageName, version string) core.Result {
- if core.Trim(registry) == "" || core.Trim(artifactPath) == "" {
- return core.Ok("")
- }
-
- dockerCommandResult := resolveImageDockerCli()
- if !dockerCommandResult.OK {
- return core.Fail(core.E("build.runBuildImage", "failed to resolve docker CLI for OCI publish", core.NewError(dockerCommandResult.Error())))
- }
- dockerCommand := dockerCommandResult.Value.(string)
-
- destinationRef := resolveOCIImageReference(registry, imageName, version)
- sourceRefResult := loadOCIImageArchive(ctx, projectDir, dockerCommand, artifactPath)
- if !sourceRefResult.OK {
- return sourceRefResult
- }
- sourceRef := sourceRefResult.Value.(string)
-
- if sourceRef != destinationRef {
- tagged := ax.ExecWithEnv(ctx, projectDir, nil, dockerCommand, "image", "tag", sourceRef, destinationRef)
- if !tagged.OK {
- return core.Fail(core.E("build.runBuildImage", "failed to tag OCI image for registry publish", core.NewError(tagged.Error())))
- }
- }
-
- pushed := ax.ExecWithEnv(ctx, projectDir, nil, dockerCommand, "image", "push", destinationRef)
- if !pushed.OK {
- return core.Fail(core.E("build.runBuildImage", "failed to push OCI image to registry", core.NewError(pushed.Error())))
- }
-
- return core.Ok(destinationRef)
-}
-
-func resolveImageDockerCli() core.Result {
- return ax.ResolveCommand("docker",
- "/usr/local/bin/docker",
- "/opt/homebrew/bin/docker",
- "/Applications/Docker.app/Contents/Resources/bin/docker",
- )
-}
-
-func resolveOCIImageReference(registry, imageName, version string) string {
- tag := normalizeImageVersionTag(version)
- if tag == "" {
- tag = "dev"
- }
-
- registry = trimTrailingImageRegistrySlashes(core.Trim(registry))
- if registry == "" {
- return imageName + ":" + tag
- }
-
- return registry + "/" + imageName + ":" + tag
-}
-
-func trimTrailingImageRegistrySlashes(registry string) string {
- for core.HasSuffix(registry, "/") {
- registry = core.TrimSuffix(registry, "/")
- }
- return registry
-}
-
-func loadOCIImageArchive(ctx context.Context, projectDir, dockerCommand, artifactPath string) core.Result {
- output := ax.CombinedOutput(ctx, projectDir, nil, dockerCommand, "image", "load", "--input", artifactPath)
- if !output.OK {
- return core.Fail(core.E("build.runBuildImage", "failed to load OCI image archive", core.NewError(output.Error())))
- }
-
- reference := parseLoadedDockerImageReference(output.Value.(string))
- if reference == "" {
- return core.Fail(core.E("build.runBuildImage", "docker image load did not report a loaded image reference", nil))
- }
-
- return core.Ok(reference)
-}
-
-func parseLoadedDockerImageReference(output string) string {
- for _, line := range core.Split(output, "\n") {
- line = core.Trim(line)
- switch {
- case core.HasPrefix(line, "Loaded image:"):
- return core.Trim(core.TrimPrefix(line, "Loaded image:"))
- case core.HasPrefix(line, "Loaded image ID:"):
- return core.Trim(core.TrimPrefix(line, "Loaded image ID:"))
- }
- }
- return ""
-}
-
-func allImageArtifactsExist(filesystem coreio.Medium, imageBuilder *builders.LinuxKitImageBuilder, outputDir, imageName string, cfg build.LinuxKitConfig, version string) bool {
- formats := normalizeImageCacheValues(cfg.Formats)
- if len(formats) == 0 {
- return false
- }
-
- for _, format := range formats {
- if !filesystem.Exists(imageBuilder.ArtifactPath(outputDir, imageName, format)) {
- return false
- }
- }
-
- metadataResult := loadImageBuildCacheMetadata(filesystem, outputDir, imageName)
- if !metadataResult.OK || metadataResult.Value == nil {
- return false
- }
- metadata, ok := metadataResult.Value.(*imageBuildCacheMetadata)
- if !ok || metadata == nil {
- return false
- }
- expected := buildImageCacheMetadata(imageName, cfg, version)
- if metadata.Signature != expected.Signature {
- return false
- }
-
- expectedVersion := core.Trim(expected.BuildVersion)
- if expectedVersion == "" {
- return true
- }
-
- return core.Trim(metadata.BuildVersion) == expectedVersion
-}
-
-func writeImageBuildCacheMetadata(filesystem coreio.Medium, outputDir, imageName string, cfg build.LinuxKitConfig, version string) core.Result {
- metadata := buildImageCacheMetadata(imageName, cfg, version)
- encoded := ax.JSONMarshal(metadata)
- if !encoded.OK {
- return encoded
- }
- return filesystem.Write(imageBuildCacheMetadataPath(outputDir, imageName), encoded.Value.(string))
-}
-
-func loadImageBuildCacheMetadata(filesystem coreio.Medium, outputDir, imageName string) core.Result {
- path := imageBuildCacheMetadataPath(outputDir, imageName)
- if !filesystem.Exists(path) {
- return core.Ok((*imageBuildCacheMetadata)(nil))
- }
-
- content := filesystem.Read(path)
- if !content.OK {
- return content
- }
-
- var metadata imageBuildCacheMetadata
- decoded := ax.JSONUnmarshal([]byte(content.Value.(string)), &metadata)
- if !decoded.OK {
- return decoded
- }
-
- return core.Ok(&metadata)
-}
-
-func imageBuildCacheMetadataPath(outputDir, imageName string) string {
- return ax.Join(outputDir, "."+imageName+"-linuxkit-image.json")
-}
-
-func buildImageCacheMetadata(imageName string, cfg build.LinuxKitConfig, version string) imageBuildCacheMetadata {
- base := cfg.Base
- baseVersion := ""
- if baseImage, ok := build.LookupLinuxKitBaseImage(base); ok {
- baseVersion = baseImage.Version
- }
-
- metadata := imageBuildCacheMetadata{
- ImageName: imageName,
- Base: base,
- BaseVersion: baseVersion,
- BuildVersion: core.Trim(version),
- Formats: normalizeImageCacheValues(cfg.Formats),
- Packages: normalizeImageCacheValues(cfg.Packages),
- Mounts: normalizeImageCacheValues(cfg.Mounts),
- GPU: cfg.GPU,
- Registry: core.Trim(cfg.Registry),
- }
- metadata.Signature = imageBuildCacheSignature(metadata)
- return metadata
-}
-
-func imageBuildCacheSignature(metadata imageBuildCacheMetadata) string {
- parts := []string{
- metadata.ImageName,
- metadata.Base,
- metadata.BaseVersion,
- core.Join(",", metadata.Formats...),
- core.Join(",", metadata.Packages...),
- core.Join(",", metadata.Mounts...),
- core.Sprintf("%t", metadata.GPU),
- metadata.Registry,
- }
-
- return core.SHA256Hex([]byte(core.Join("\n", parts...)))
-}
-
-func normalizeImageCacheValues(values []string) []string {
- if len(values) == 0 {
- return nil
- }
-
- seen := make(map[string]struct{}, len(values))
- result := make([]string, 0, len(values))
- for _, value := range values {
- value = core.Trim(value)
- if value == "" {
- continue
- }
- if _, ok := seen[value]; ok {
- continue
- }
- seen[value] = struct{}{}
- result = append(result, value)
- }
-
- slices.SortFunc(result, func(a, b string) int {
- if a < b {
- return -1
- }
- if a > b {
- return 1
- }
- return 0
- })
- return result
-}
diff --git a/cmd/build/cmd_image_example_test.go b/cmd/build/cmd_image_example_test.go
deleted file mode 100644
index 946e0b8..0000000
--- a/cmd/build/cmd_image_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package buildcmd
-
-import core "dappco.re/go"
-
-// ExampleAddImageCommand references AddImageCommand on this package API surface.
-func ExampleAddImageCommand() {
- _ = AddImageCommand
- core.Println("AddImageCommand")
- // Output: AddImageCommand
-}
diff --git a/cmd/build/cmd_image_test.go b/cmd/build/cmd_image_test.go
deleted file mode 100644
index f14f480..0000000
--- a/cmd/build/cmd_image_test.go
+++ /dev/null
@@ -1,385 +0,0 @@
-package buildcmd
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- "dappco.re/go/build/pkg/build/builders"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func setupFakeLinuxKitImageCLI(t *testing.T, binDir string) {
- t.Helper()
-
- script := `#!/bin/sh
-set -eu
-
-format=""
-dir=""
-name=""
-while [ $# -gt 0 ]; do
- case "$1" in
- build)
- ;;
- --format)
- shift
- format="${1:-}"
- ;;
- --dir)
- shift
- dir="${1:-}"
- ;;
- --name)
- shift
- name="${1:-}"
- ;;
- esac
- shift
-done
-
-ext=".img"
-case "$format" in
- tar)
- ext=".tar"
- ;;
- iso|iso-bios|iso-efi)
- ext=".iso"
- ;;
-esac
-
-mkdir -p "$dir"
-printf 'linuxkit image\n' > "$dir/$name$ext"
-`
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(binDir, "linuxkit"), []byte(script), 0o755))
-
-}
-
-func setupFakeDockerImageCLI(t *testing.T, binDir string) {
- t.Helper()
-
- script := `#!/bin/sh
-set -eu
-
-log_file="${DOCKER_LOG:-}"
-
-record() {
- if [ -n "$log_file" ]; then
- printf '%s\n' "$1" >> "$log_file"
- fi
-}
-
-case "${1:-}" in
- build)
- shift
- record "docker build $*"
- ;;
- image)
- shift
- case "${1:-}" in
- load)
- shift
- record "docker image load $*"
- echo "Loaded image: imported:latest"
- ;;
- tag)
- shift
- record "docker image tag $*"
- ;;
- push)
- shift
- record "docker image push $*"
- ;;
- *)
- record "docker image $*"
- ;;
- esac
- ;;
- *)
- record "docker $*"
- ;;
-esac
-`
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(binDir, "docker"), []byte(script), 0o755))
-
-}
-
-func TestBuildCmd_AddImageCommand_Good(t *testing.T) {
- c := core.New()
-
- AddImageCommand(c)
- if !(c.Command("build/image").OK) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestBuildCmd_parseImageFormats_Good(t *testing.T) {
- if !stdlibAssertEqual([]string{"oci", "apple"}, parseImageFormats(" OCI , apple,Apple, oci ")) {
- t.Fatalf("want %v, got %v", []string{"oci", "apple"}, parseImageFormats(" OCI , apple,Apple, oci "))
- }
-
-}
-
-func TestBuildCmd_buildPwaCommandAcceptsPathGood(t *testing.T) {
- c := core.New()
- AddBuildCommands(c)
-
- command := c.Command("build/pwa").Value.(*core.Command)
-
- original := runLocalPwaBuild
- defer func() { runLocalPwaBuild = original }()
-
- calledPath := ""
- runLocalPwaBuild = func(ctx context.Context, projectDir string) core.Result {
- calledPath = projectDir
- return core.Ok(nil)
- }
-
- opts := core.NewOptions(core.Option{Key: buildPathOptionKey, Value: "/tmp/pwa"})
- result := command.Run(opts)
- if !(result.OK) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("/tmp/pwa", calledPath) {
- t.Fatalf("want %v, got %v", "/tmp/pwa", calledPath)
- }
-
-}
-
-func TestBuildCmd_runBuildImage_Good(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeLinuxKitImageCLI(t, binDir)
- setupFakeDockerImageCLI(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- outputDir := t.TempDir()
-
- requireBuildCmdOK(t, runBuildImage(ImageBuildRequest{
- Context: context.Background(),
- Base: "core-minimal",
- Format: "oci,apple",
- OutputDir: outputDir,
- }))
- requireBuildCmdOK(t, ax.Stat(ax.Join(outputDir, "core-minimal.tar")))
- requireBuildCmdOK(t, ax.Stat(ax.Join(outputDir, "core-minimal.aci")))
-
- t.Setenv("PATH", "/definitely-missing")
- requireBuildCmdOK(t, runBuildImage(ImageBuildRequest{
- Context: context.Background(),
- Base: "core-minimal",
- Format: "oci,apple",
- OutputDir: outputDir,
- }))
-
-}
-
-func TestBuildCmd_resolveImmutableImageVersion_Good(t *testing.T) {
- t.Run("uses exact release tag on HEAD", func(t *testing.T) {
- dir := t.TempDir()
-
- runGit(t, dir, "init")
- runGit(t, dir, "config", "user.email", "test@example.com")
- runGit(t, dir, "config", "user.name", "Test User")
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(dir, "README.md"), []byte("hello\n"), 0o644))
-
- runGit(t, dir, "add", ".")
- runGit(t, dir, "commit", "-m", "feat: initial commit")
- runGit(t, dir, "tag", "v1.4.2")
-
- version := resolveImmutableImageVersion(context.Background(), dir)
- if !stdlibAssertEqual(immutableImageVersion{BuildVersion: "v1.4.2", RetainVersion: "v1.4.2", CacheVersion: "v1.4.2"}, version) {
- t.Fatalf("want %v, got %v", immutableImageVersion{BuildVersion: "v1.4.2", RetainVersion: "v1.4.2", CacheVersion: "v1.4.2"}, version)
- }
-
- })
-
- t.Run("falls back to dev for untagged commits", func(t *testing.T) {
- dir := t.TempDir()
-
- runGit(t, dir, "init")
- runGit(t, dir, "config", "user.email", "test@example.com")
- runGit(t, dir, "config", "user.name", "Test User")
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(dir, "README.md"), []byte("hello\n"), 0o644))
-
- runGit(t, dir, "add", ".")
- runGit(t, dir, "commit", "-m", "feat: initial commit")
-
- version := resolveImmutableImageVersion(context.Background(), dir)
- if !stdlibAssertEqual(immutableImageVersion{BuildVersion: "dev"}, version) {
- t.Fatalf("want %v, got %v", immutableImageVersion{BuildVersion: "dev"}, version)
- }
-
- })
-
- t.Run("falls back to dev after the release tag moves behind HEAD", func(t *testing.T) {
- dir := t.TempDir()
-
- runGit(t, dir, "init")
- runGit(t, dir, "config", "user.email", "test@example.com")
- runGit(t, dir, "config", "user.name", "Test User")
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(dir, "README.md"), []byte("hello\n"), 0o644))
-
- runGit(t, dir, "add", ".")
- runGit(t, dir, "commit", "-m", "feat: initial commit")
- runGit(t, dir, "tag", "v1.4.2")
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(dir, "CHANGELOG.md"), []byte("more\n"), 0o644))
-
- runGit(t, dir, "add", ".")
- runGit(t, dir, "commit", "-m", "feat: follow-up work")
-
- version := resolveImmutableImageVersion(context.Background(), dir)
- if !stdlibAssertEqual(immutableImageVersion{BuildVersion: "dev"}, version) {
- t.Fatalf("want %v, got %v", immutableImageVersion{BuildVersion: "dev"}, version)
- }
-
- })
-}
-
-func TestBuildCmd_allImageArtifactsExist_RequiresMatchingCacheMetadata_Good(t *testing.T) {
- outputDir := t.TempDir()
- imageName := "core-dev"
- builder := builders.NewLinuxKitImageBuilder()
- cfg := build.LinuxKitConfig{
- Base: "core-dev",
- Formats: []string{"oci", "apple"},
- Packages: []string{"git", "task"},
- Mounts: []string{"/workspace"},
- }
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(outputDir, "core-dev.tar"), []byte("oci image"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(outputDir, "core-dev.aci"), []byte("apple image"), 0o644))
- requireBuildCmdOK(t, writeImageBuildCacheMetadata(storage.Local, outputDir, imageName, cfg, "v1.2.3"))
- if !(allImageArtifactsExist(storage.Local, builder, outputDir, imageName, cfg, "v1.2.3")) {
- t.Fatal("expected true")
- }
- if allImageArtifactsExist(storage.Local, builder, outputDir, imageName, cfg, "v1.2.4") {
- t.Fatal("expected false")
- }
-
- changedCfg := cfg
- changedCfg.GPU = true
- if allImageArtifactsExist(storage.Local, builder, outputDir, imageName, changedCfg, "v1.2.3") {
- t.Fatal("expected false")
- }
- requireBuildCmdOK(t, storage.Local.Delete(imageBuildCacheMetadataPath(outputDir, imageName)))
- if allImageArtifactsExist(storage.Local, builder, outputDir, imageName, cfg, "v1.2.3") {
- t.Fatal("expected false")
- }
-
-}
-
-func TestBuildCmd_allImageArtifactsExist_ValidatesVersionlessCacheMetadata_Good(t *testing.T) {
- outputDir := t.TempDir()
- imageName := "core-dev"
- builder := builders.NewLinuxKitImageBuilder()
- cfg := build.LinuxKitConfig{
- Base: "core-dev",
- Formats: []string{"oci", "apple"},
- Packages: []string{"git", "task"},
- Mounts: []string{"/workspace"},
- }
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(outputDir, "core-dev.tar"), []byte("oci image"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(outputDir, "core-dev.aci"), []byte("apple image"), 0o644))
- requireBuildCmdOK(t, writeImageBuildCacheMetadata(storage.Local, outputDir, imageName, cfg, ""))
- if !(allImageArtifactsExist(storage.Local, builder, outputDir, imageName, cfg, "")) {
- t.Fatal("expected true")
- }
-
- changedCfg := cfg
- changedCfg.GPU = true
- if allImageArtifactsExist(storage.Local, builder, outputDir, imageName, changedCfg, "") {
- t.Fatal("expected false")
- }
-
-}
-
-func TestBuildCmd_retainVersionedImageArtifacts_Good(t *testing.T) {
- outputDir := t.TempDir()
- tarPath := ax.Join(outputDir, "core-dev.tar")
- aciPath := ax.Join(outputDir, "core-dev.aci")
- requireBuildCmdOK(t, ax.WriteFile(tarPath, []byte("oci image"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(aciPath, []byte("apple image"), 0o644))
-
- versionedPathsResult := retainVersionedImageArtifacts(storage.Local, []build.Artifact{
- {Path: tarPath},
- {Path: aciPath},
- }, "v1.2.3")
- requireBuildCmdOK(t, versionedPathsResult)
- versionedPaths := versionedPathsResult.Value.([]string)
-
- expected := []string{
- ax.Join(outputDir, "core-dev-1.2.3.tar"),
- ax.Join(outputDir, "core-dev-1.2.3.aci"),
- }
- if !stdlibAssertElementsMatch(expected, versionedPaths) {
- t.Fatalf("expected elements %v, got %v", expected, versionedPaths)
- }
-
- for _, path := range expected {
- requireBuildCmdOK(t, ax.Stat(path))
-
- }
-}
-
-func TestBuildCmd_publishOCIImageArchive_Good(t *testing.T) {
- binDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "docker.log")
- setupFakeDockerImageCLI(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
- t.Setenv("DOCKER_LOG", logPath)
-
- projectDir := t.TempDir()
- artifactPath := ax.Join(projectDir, "core-dev.tar")
- requireBuildCmdOK(t, ax.WriteFile(artifactPath, []byte("oci image"), 0o644))
-
- ref := requireBuildCmdString(t, publishOCIImageArchive(context.Background(), projectDir, artifactPath, "ghcr.io/dappcore", "core-dev", "v1.2.3"))
- if !stdlibAssertEqual("ghcr.io/dappcore/core-dev:1.2.3", ref) {
- t.Fatalf("want %v, got %v", "ghcr.io/dappcore/core-dev:1.2.3", ref)
- }
-
- logContent := requireBuildCmdBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(logContent), "docker image load --input "+artifactPath) {
- t.Fatalf("expected %v to contain %v", string(logContent), "docker image load --input "+artifactPath)
- }
- if !stdlibAssertContains(string(logContent), "docker image tag imported:latest ghcr.io/dappcore/core-dev:1.2.3") {
- t.Fatalf("expected %v to contain %v", string(logContent), "docker image tag imported:latest ghcr.io/dappcore/core-dev:1.2.3")
- }
- if !stdlibAssertContains(string(logContent), "docker image push ghcr.io/dappcore/core-dev:1.2.3") {
- t.Fatalf("expected %v to contain %v", string(logContent), "docker image push ghcr.io/dappcore/core-dev:1.2.3")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCmdImage_AddImageCommand_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- AddImageCommand(core.New())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCmdImage_AddImageCommand_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- AddImageCommand(core.New())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCmdImage_AddImageCommand_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- AddImageCommand(core.New())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/cmd/build/cmd_installers.go b/cmd/build/cmd_installers.go
deleted file mode 100644
index e33fbf2..0000000
--- a/cmd/build/cmd_installers.go
+++ /dev/null
@@ -1,204 +0,0 @@
-package buildcmd
-
-import (
- "context"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/internal/cmdutil"
- "dappco.re/go/build/pkg/build"
- buildinstallers "dappco.re/go/build/pkg/build/installers"
- "dappco.re/go/build/pkg/release"
- "dappco.re/go/build/pkg/release/publishers"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-var (
- getInstallersWorkingDir = ax.Getwd
- loadInstallersBuildConfig = build.LoadConfig
- loadInstallersReleaseConfig = release.LoadConfig
- resolveInstallersVersion = resolveBuildVersion
- detectInstallersRepository = publishers.DetectGitHubRepository
-)
-
-// BuildInstallersRequest groups the inputs for `core build installers`.
-type BuildInstallersRequest struct {
- Context context.Context
- Variant string
- Version string
- OutputDir string
- Repo string
- BinaryName string
-}
-
-// AddInstallersCommand registers the installer generation command.
-func AddInstallersCommand(c *core.Core) {
- c.Command("build/installers", core.Command{
- Description: "Generate installer scripts",
- Action: func(opts core.Options) core.Result {
- return runBuildInstallers(BuildInstallersRequest{
- Context: cmdutil.ContextOrBackground(),
- Variant: cmdutil.OptionString(opts, "variant"),
- Version: cmdutil.OptionString(opts, "version"),
- OutputDir: cmdutil.OptionString(opts, "output"),
- Repo: cmdutil.OptionString(opts, "repo"),
- BinaryName: cmdutil.OptionString(opts, "name", "binary"),
- })
- },
- })
-}
-
-func runBuildInstallers(req BuildInstallersRequest) core.Result {
- ctx := req.Context
- if ctx == nil {
- ctx = context.Background()
- }
-
- projectDirResult := getInstallersWorkingDir()
- if !projectDirResult.OK {
- return core.Fail(core.E("build.runBuildInstallers", "failed to get working directory", core.NewError(projectDirResult.Error())))
- }
-
- return runBuildInstallersInDir(ctx, projectDirResult.Value.(string), req.Variant, req.Version, req.OutputDir, req.Repo, req.BinaryName)
-}
-
-func runBuildInstallersInDir(ctx context.Context, projectDir, variant, version, outputDir, repo, binaryName string) core.Result {
- filesystem := storage.Local
-
- buildConfigResult := loadInstallersBuildConfig(filesystem, projectDir)
- if !buildConfigResult.OK {
- return core.Fail(core.E("build.runBuildInstallers", "failed to load build config", core.NewError(buildConfigResult.Error())))
- }
- buildConfig := buildConfigResult.Value.(*build.BuildConfig)
-
- installerVersion := core.Trim(version)
- if installerVersion == "" {
- versionResult := resolveInstallersVersion(ctx, projectDir)
- if !versionResult.OK {
- return core.Fail(core.E("build.runBuildInstallers", "failed to determine installer version; use --version to override", core.NewError(versionResult.Error())))
- }
- installerVersion = versionResult.Value.(string)
- }
- validVersion := build.ValidateVersionIdentifier(installerVersion)
- if !validVersion.OK {
- return core.Fail(core.E("build.runBuildInstallers", "invalid installer version; use a safe release identifier", core.NewError(validVersion.Error())))
- }
-
- installerRepo := core.Trim(repo)
- if installerRepo == "" {
- repoResult := resolveInstallersRepository(ctx, projectDir)
- if !repoResult.OK {
- return repoResult
- }
- installerRepo = repoResult.Value.(string)
- }
-
- if outputDir == "" {
- outputDir = ax.Join(projectDir, "dist", "installers")
- } else if !ax.IsAbs(outputDir) {
- outputDir = ax.Join(projectDir, outputDir)
- }
-
- created := filesystem.EnsureDir(outputDir)
- if !created.OK {
- return core.Fail(core.E("build.runBuildInstallers", "failed to create output directory", core.NewError(created.Error())))
- }
-
- cfg := buildinstallers.InstallerConfig{
- Version: installerVersion,
- Repo: installerRepo,
- BinaryName: build.ResolveBuildName(projectDir, buildConfig, binaryName),
- }
-
- normalizedVariant, ok := normalizeInstallersVariant(variant)
- if !ok {
- return core.Fail(core.E("build.runBuildInstallers", "unknown installer variant: "+core.Trim(variant), nil))
- }
-
- cli.Print("%s %s\n", buildHeaderStyle.Render("Installers"), "generating installer scripts")
-
- if normalizedVariant != "" {
- return writeInstallerVariant(filesystem, projectDir, outputDir, normalizedVariant, cfg)
- }
-
- for _, candidate := range build.InstallerVariants() {
- written := writeInstallerVariant(filesystem, projectDir, outputDir, candidate, cfg)
- if !written.OK {
- return written
- }
- }
-
- return core.Ok(nil)
-}
-
-func writeInstallerVariant(filesystem storage.Medium, projectDir, outputDir string, variant build.InstallerVariant, cfg buildinstallers.InstallerConfig) core.Result {
- scriptName := build.InstallerOutputName(variant)
- if scriptName == "" {
- return core.Fail(core.E("build.writeInstallerVariant", "unknown installer variant: "+string(variant), nil))
- }
-
- scriptResult := buildinstallers.GenerateInstaller(variant, cfg)
- if !scriptResult.OK {
- return core.Fail(core.E("build.writeInstallerVariant", "failed to generate "+scriptName, core.NewError(scriptResult.Error())))
- }
- script := scriptResult.Value.(string)
-
- targetPath := ax.Join(outputDir, scriptName)
- written := filesystem.WriteMode(targetPath, script, 0o755)
- if !written.OK {
- return core.Fail(core.E("build.writeInstallerVariant", "failed to write "+scriptName, core.NewError(written.Error())))
- }
-
- relPath := targetPath
- relPathResult := ax.Rel(projectDir, targetPath)
- if relPathResult.OK {
- relPath = relPathResult.Value.(string)
- }
- cli.Print(" %s\n", relPath)
-
- return core.Ok(nil)
-}
-
-func resolveInstallersRepository(ctx context.Context, projectDir string) core.Result {
- releaseConfigResult := loadInstallersReleaseConfig(projectDir)
- if !releaseConfigResult.OK {
- return core.Fail(core.E("build.resolveInstallersRepository", "failed to load release config", core.NewError(releaseConfigResult.Error())))
- }
- releaseConfig := releaseConfigResult.Value.(*release.Config)
-
- if releaseConfig != nil {
- repo := core.Trim(releaseConfig.GetRepository())
- if repo != "" {
- return core.Ok(repo)
- }
- }
-
- repoResult := detectInstallersRepository(ctx, projectDir)
- if !repoResult.OK {
- return core.Fail(core.E("build.resolveInstallersRepository", "failed to determine repository; use --repo or configure .core/release.yaml project.repository", core.NewError(repoResult.Error())))
- }
-
- return repoResult
-}
-
-func normalizeInstallersVariant(value string) (build.InstallerVariant, bool) {
- switch core.Lower(core.Trim(value)) {
- case "", "all":
- return "", true
- case "full", "setup", "setup.sh":
- return build.VariantFull, true
- case "ci", "ci.sh":
- return build.VariantCI, true
- case "php", "php.sh":
- return build.VariantPHP, true
- case "go", "go.sh":
- return build.VariantGo, true
- case "agent", "agentic", "agent.sh":
- return build.VariantAgent, true
- case "dev", "dev.sh":
- return build.VariantDev, true
- default:
- return "", false
- }
-}
diff --git a/cmd/build/cmd_installers_example_test.go b/cmd/build/cmd_installers_example_test.go
deleted file mode 100644
index 0f91a1b..0000000
--- a/cmd/build/cmd_installers_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package buildcmd
-
-import core "dappco.re/go"
-
-// ExampleAddInstallersCommand references AddInstallersCommand on this package API surface.
-func ExampleAddInstallersCommand() {
- _ = AddInstallersCommand
- core.Println("AddInstallersCommand")
- // Output: AddInstallersCommand
-}
diff --git a/cmd/build/cmd_installers_test.go b/cmd/build/cmd_installers_test.go
deleted file mode 100644
index 87248fb..0000000
--- a/cmd/build/cmd_installers_test.go
+++ /dev/null
@@ -1,235 +0,0 @@
-package buildcmd
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- "dappco.re/go/build/pkg/release"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func TestBuildCmd_AddInstallersCommand_Good(t *testing.T) {
- c := core.New()
-
- AddInstallersCommand(c)
- if !(c.Command("build/installers").OK) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestBuildCmd_runBuildInstallersInDir_GeneratesAll_Good(t *testing.T) {
- projectDir := t.TempDir()
- requireBuildCmdOK(t, storage.Local.EnsureDir(ax.Join(projectDir, ".core")))
- requireBuildCmdOK(t, storage.Local.Write(ax.Join(projectDir, ".core", "build.yaml"), `version: 1
-project:
- binary: corex
-`))
- requireBuildCmdOK(t, storage.Local.Write(ax.Join(projectDir, ".core", "release.yaml"), `version: 1
-project:
- repository: dappcore/core
-`))
-
- requireBuildCmdOK(t, runBuildInstallersInDir(context.Background(), projectDir, "", "v1.2.3", "", "", ""))
-
- outputDir := ax.Join(projectDir, "dist", "installers")
- expected := []string{"setup.sh", "ci.sh", "php.sh", "go.sh", "agent.sh", "dev.sh"}
- for _, name := range expected {
- requireBuildCmdOK(t, ax.Stat(ax.Join(outputDir, name)))
-
- }
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(outputDir, "setup.sh")))
- if !stdlibAssertContains(content, "corex") {
- t.Fatalf("expected %v to contain %v", content, "corex")
- }
- if !stdlibAssertContains(content, "v1.2.3") {
- t.Fatalf("expected %v to contain %v", content, "v1.2.3")
- }
- if !stdlibAssertContains(content, "dappcore/core") {
- t.Fatalf("expected %v to contain %v", content, "dappcore/core")
- }
- if !stdlibAssertContains(content, "https://lthn.sh/setup.sh") {
- t.Fatalf("expected %v to contain %v", content, "https://lthn.sh/setup.sh")
- }
-
- devContent := requireBuildCmdString(t, storage.Local.Read(ax.Join(outputDir, "dev.sh")))
- if !stdlibAssertContains(devContent, `DEV_IMAGE_VERSION="${VERSION#v}"`) {
- t.Fatalf("expected %v to contain %v", devContent, `DEV_IMAGE_VERSION="${VERSION#v}"`)
- }
- if !stdlibAssertContains(devContent, `DEV_IMAGE="ghcr.io/dappcore/core-dev:${DEV_IMAGE_VERSION}"`) {
- t.Fatalf("expected %v to contain %v", devContent, `DEV_IMAGE="ghcr.io/dappcore/core-dev:${DEV_IMAGE_VERSION}"`)
- }
-
-}
-
-func TestBuildCmd_runBuildInstallersInDir_GeneratesSingleVariant_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- requireBuildCmdOK(t, runBuildInstallersInDir(context.Background(), projectDir, "ci", "v1.2.3", "out/installers", "dappcore/core", "core"))
- requireBuildCmdOK(t, ax.Stat(ax.Join(projectDir, "out", "installers", "ci.sh")))
- if ax.Exists(ax.Join(projectDir, "out", "installers", "setup.sh")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(projectDir, "out", "installers", "setup.sh"))
- }
-
-}
-
-func TestBuildCmd_runBuildInstallersInDir_UsesResolvedVersion_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- originalVersionResolver := resolveInstallersVersion
- t.Cleanup(func() {
- resolveInstallersVersion = originalVersionResolver
- })
- resolveInstallersVersion = func(ctx context.Context, dir string) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
-
- return core.Ok("v9.9.9")
- }
-
- requireBuildCmdOK(t, runBuildInstallersInDir(context.Background(), projectDir, "setup.sh", "", "", "dappcore/core", "core"))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, "dist", "installers", "setup.sh")))
- if !stdlibAssertContains(content, "v9.9.9") {
- t.Fatalf("expected %v to contain %v", content, "v9.9.9")
- }
-
-}
-
-func TestBuildCmd_runBuildInstallersInDir_UsesGitRemoteWhenReleaseConfigMissing_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- originalLoadReleaseConfig := loadInstallersReleaseConfig
- originalDetectRepository := detectInstallersRepository
- t.Cleanup(func() {
- loadInstallersReleaseConfig = originalLoadReleaseConfig
- detectInstallersRepository = originalDetectRepository
- })
-
- loadInstallersReleaseConfig = func(dir string) core.Result {
- cfg := release.DefaultConfig()
- cfg.SetProjectDir(dir)
- return core.Ok(cfg)
- }
- detectInstallersRepository = func(ctx context.Context, dir string) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
-
- return core.Ok("host-uk/core-build")
- }
-
- requireBuildCmdOK(t, runBuildInstallersInDir(context.Background(), projectDir, "agentic", "v1.2.3", "", "", "core"))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, "dist", "installers", "agent.sh")))
- if !stdlibAssertContains(content, "host-uk/core-build") {
- t.Fatalf("expected %v to contain %v", content, "host-uk/core-build")
- }
-
-}
-
-func TestBuildCmd_runBuildInstallersInDir_UnknownVariant_Bad(t *testing.T) {
- projectDir := t.TempDir()
-
- message := requireBuildCmdError(t, runBuildInstallersInDir(context.Background(), projectDir, "bogus", "v1.2.3", "", "dappcore/core", "core"))
- if !stdlibAssertContains(message, "unknown installer variant") {
- t.Fatalf("expected %v to contain %v", message, "unknown installer variant")
- }
-
-}
-
-func TestBuildCmd_runBuildInstallersInDir_RejectsUnsafeVersion_Bad(t *testing.T) {
- projectDir := t.TempDir()
-
- message := requireBuildCmdError(t, runBuildInstallersInDir(context.Background(), projectDir, "ci", "v1.2.3 --bad", "", "dappcore/core", "core"))
- if !stdlibAssertContains(message, "invalid installer version") {
- t.Fatalf("expected %v to contain %v", message, "invalid installer version")
- }
-
-}
-
-func TestBuildCmd_runBuildInstallersInDir_MissingRepository_Bad(t *testing.T) {
- projectDir := t.TempDir()
-
- originalLoadReleaseConfig := loadInstallersReleaseConfig
- originalDetectRepository := detectInstallersRepository
- t.Cleanup(func() {
- loadInstallersReleaseConfig = originalLoadReleaseConfig
- detectInstallersRepository = originalDetectRepository
- })
-
- loadInstallersReleaseConfig = func(dir string) core.Result {
- cfg := release.DefaultConfig()
- cfg.SetProjectDir(dir)
- return core.Ok(cfg)
- }
- detectInstallersRepository = func(ctx context.Context, dir string) core.Result {
- return core.Fail(core.NewError("test error"))
- }
-
- message := requireBuildCmdError(t, runBuildInstallersInDir(context.Background(), projectDir, "ci", "v1.2.3", "", "", "core"))
- if !stdlibAssertContains(message, "use --repo") {
- t.Fatalf("expected %v to contain %v", message, "use --repo")
- }
-
-}
-
-func TestBuild_GenerateInstallerWrappersGood(t *testing.T) {
- script := requireBuildCmdString(t, build.GenerateInstaller(build.VariantCI, "v1.2.3", "dappcore/core"))
- if !stdlibAssertContains(script, "dappcore/core") {
- t.Fatalf("expected %v to contain %v", script, "dappcore/core")
- }
- if !stdlibAssertEqual([]build.InstallerVariant{build.VariantFull, build.VariantCI, build.VariantPHP, build.VariantGo, build.VariantAgent, build.VariantDev}, build.InstallerVariants()) {
- t.Fatalf("want %v, got %v", []build.InstallerVariant{build.VariantFull, build.VariantCI, build.VariantPHP, build.VariantGo, build.VariantAgent, build.VariantDev}, build.InstallerVariants())
- }
- if !stdlibAssertEqual("ci.sh", build.InstallerOutputName(build.VariantCI)) {
- t.Fatalf("want %v, got %v", "ci.sh", build.InstallerOutputName(build.VariantCI))
- }
- if !stdlibAssertEqual(build.VariantAgent, build.VariantAgentic) {
- t.Fatalf("want %v, got %v", build.VariantAgent, build.VariantAgentic)
- }
-
- agenticScript := requireBuildCmdString(t, build.GenerateInstaller(build.VariantAgentic, "v1.2.3", "dappcore/core"))
- if !stdlibAssertContains(agenticScript, "dappcore/core") {
- t.Fatalf("expected %v to contain %v", agenticScript, "dappcore/core")
- }
-
- scripts := requireBuildCmdStringMap(t, build.GenerateAll("v1.2.3", "dappcore/core"))
- if !stdlibAssertContains(scripts["setup.sh"], "dappcore/core") {
- t.Fatalf("expected %v to contain %v", scripts["setup.sh"], "dappcore/core")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCmdInstallers_AddInstallersCommand_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- AddInstallersCommand(core.New())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCmdInstallers_AddInstallersCommand_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- AddInstallersCommand(core.New())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCmdInstallers_AddInstallersCommand_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- AddInstallersCommand(core.New())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/cmd/build/cmd_project.go b/cmd/build/cmd_project.go
deleted file mode 100644
index be4e695..0000000
--- a/cmd/build/cmd_project.go
+++ /dev/null
@@ -1,805 +0,0 @@
-// cmd_project.go implements project build orchestration and auto-detection.
-//
-// runProjectBuild(ProjectBuildRequest{
-// BuildType: "go",
-// TargetsFlag: "linux/amd64,darwin/arm64",
-// ArchiveOutput: true,
-// }) executes end-to-end build/sign/archive/checksum flow for the selected project.
-
-package buildcmd
-
-import (
- "context"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/pkg/build"
- "dappco.re/go/build/pkg/build/builders"
- "dappco.re/go/build/pkg/build/signing"
- "dappco.re/go/build/pkg/release"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-var getProjectBuildWorkingDir = ax.Getwd
-
-// ProjectBuildRequest groups the inputs for the main `core build` command.
-//
-// req := ProjectBuildRequest{
-// Context: cmd.Context(),
-// BuildType: "go",
-// TargetsFlag: "linux/amd64,linux/arm64",
-// }
-type ProjectBuildRequest struct {
- Context context.Context
- BuildType string
- Version string
- CIMode bool
- TargetsFlag string
- OutputDir string
- BuildName string
- BuildTagsFlag string
- Obfuscate bool
- ObfuscateSet bool
- NSIS bool
- NSISSet bool
- WebView2 string
- WebView2Set bool
- DenoBuild string
- DenoBuildSet bool
- NpmBuild string
- NpmBuildSet bool
- BuildCache bool
- BuildCacheSet bool
- ArchiveOutput bool
- ArchiveOutputSet bool
- ChecksumOutput bool
- ChecksumOutputSet bool
- PackageSet bool
- ArchiveFormat string
- ConfigPath string
- Format string
- Push bool
- ImageName string
- Sign bool
- SignSet bool
- NoSign bool
- Notarize bool
- Verbose bool
-}
-
-// runProjectBuild handles the main `core build` command with auto-detection.
-//
-// runProjectBuild(ProjectBuildRequest{
-// BuildType: "node",
-// TargetsFlag: "linux/amd64",
-// ArchiveOutput: true,
-// ChecksumOutput: true,
-// Format: "gz",
-// })
-func runProjectBuild(req ProjectBuildRequest) (result core.Result) {
- if req.CIMode {
- defer func() {
- emitCIErrorAnnotation(result)
- }()
- }
-
- ctx := req.Context
- if ctx == nil {
- ctx = context.Background()
- }
- // Use local filesystem as the default medium.
- filesystem := storage.Local
-
- // Get current working directory as project root
- projectDirResult := getProjectBuildWorkingDir()
- if !projectDirResult.OK {
- return core.Fail(core.E("build.Run", "failed to get working directory", core.NewError(projectDirResult.Error())))
- }
- projectDir := projectDirResult.Value.(string)
-
- // PWA builds use the dedicated local web-app pipeline rather than the
- // project-type builder registry.
- if req.BuildType == "pwa" {
- return runLocalPwaBuild(ctx, projectDir)
- }
-
- if shouldUseGoBuildPassthrough(filesystem, projectDir, req) {
- return runGoBuildPassthrough(ctx, projectDir, req)
- }
-
- // Load configuration from .core/build.yaml (or defaults)
- var buildConfig *build.BuildConfig
- configPath := req.ConfigPath
- if configPath != "" {
- if !ax.IsAbs(configPath) {
- configPath = ax.Join(projectDir, configPath)
- }
- if !filesystem.Exists(configPath) {
- return core.Fail(core.E("build.Run", "build config not found: "+configPath, nil))
- }
- configResult := build.LoadConfigAtPath(filesystem, configPath)
- if !configResult.OK {
- return core.Fail(core.E("build.Run", "failed to load config", core.NewError(configResult.Error())))
- }
- buildConfig = configResult.Value.(*build.BuildConfig)
- } else {
- configResult := build.LoadConfig(filesystem, projectDir)
- if !configResult.OK {
- return core.Fail(core.E("build.Run", "failed to load config", core.NewError(configResult.Error())))
- }
- buildConfig = configResult.Value.(*build.BuildConfig)
- }
-
- if buildConfig.Build.Type == "pwa" {
- return runLocalPwaBuild(ctx, projectDir)
- }
-
- applyProjectBuildOverrides(buildConfig, req)
-
- // Determine targets
- var buildTargets []build.Target
- if req.TargetsFlag != "" {
- // Parse from command line
- targetsResult := parseTargets(req.TargetsFlag)
- if !targetsResult.OK {
- return targetsResult
- }
- buildTargets = targetsResult.Value.([]build.Target)
- } else if len(buildConfig.Targets) > 0 {
- // Use config targets
- buildTargets = buildConfig.ToTargets()
- } else {
- // Fall back to current OS/arch
- buildTargets = []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
- }
-
- pipeline := &build.Pipeline{
- FS: filesystem,
- ResolveBuilder: getBuilder,
- ResolveVersion: resolveBuildVersion,
- }
- planResult := pipeline.Plan(ctx, build.PipelineRequest{
- ProjectDir: projectDir,
- BuildConfig: buildConfig,
- BuildType: req.BuildType,
- Version: req.Version,
- OutputDir: req.OutputDir,
- BuildName: req.BuildName,
- Targets: buildTargets,
- Push: req.Push,
- ImageName: req.ImageName,
- })
- if !planResult.OK {
- return planResult
- }
- plan := planResult.Value.(*build.PipelinePlan)
-
- // Print build info (verbose mode only)
- if req.Verbose && !req.CIMode {
- cli.Print("%s %s\n", buildHeaderStyle.Render("Build"), "Building project")
- cli.Print(" %s %s\n", "type", buildTargetStyle.Render(formatProjectTypes(plan.ProjectTypes)))
- cli.Print(" %s %s\n", "output", buildTargetStyle.Render(plan.OutputDir))
- cli.Print(" %s %s\n", "binary", buildTargetStyle.Render(plan.BuildName))
- cli.Print(" %s %s\n", "targets", buildTargetStyle.Render(formatTargets(plan.Targets)))
- cli.Blank()
- }
-
- // Parse formats for LinuxKit
- if req.Format != "" {
- plan.RuntimeConfig.Formats = core.Split(req.Format, ",")
- }
-
- // Execute build
- pipelineResultValue := pipeline.Run(ctx, plan)
- if !pipelineResultValue.OK {
- if !req.CIMode {
- cli.Print("%s %v\n", buildErrorStyle.Render("error"), pipelineResultValue.Error())
- }
- return pipelineResultValue
- }
- pipelineResult := pipelineResultValue.Value.(*build.PipelineResult)
- artifacts := pipelineResult.Artifacts
- if req.CIMode {
- rewritten := rewriteArtifactsForCI(filesystem, plan.BuildName, artifacts)
- if !rewritten.OK {
- return rewritten
- }
- artifacts = rewritten.Value.([]build.Artifact)
- }
-
- if req.Verbose && !req.CIMode {
- cli.Print("%s %s\n", buildSuccessStyle.Render("Success"), core.Sprintf("Built %d artifacts", len(artifacts)))
- cli.Blank()
- for _, artifact := range artifacts {
- relPath := artifact.Path
- relPathResult := ax.Rel(projectDir, artifact.Path)
- if relPathResult.OK {
- relPath = relPathResult.Value.(string)
- }
- cli.Print(" %s %s %s\n",
- buildSuccessStyle.Render("*"),
- buildTargetStyle.Render(relPath),
- buildDimStyle.Render(core.Sprintf("(%s/%s)", artifact.OS, artifact.Arch)),
- )
- }
- }
-
- // Sign binaries if enabled.
- signCfg := resolveBuildSignConfig(plan.BuildConfig.Sign, req)
-
- if signCfg.Enabled && (runtime.GOOS == "darwin" || runtime.GOOS == "windows") {
- if req.Verbose && !req.CIMode {
- cli.Blank()
- cli.Print("%s %s\n", buildHeaderStyle.Render("Sign"), "Signing binaries")
- }
-
- // Convert build.Artifact to signing.Artifact
- signingArtifacts := make([]signing.Artifact, len(artifacts))
- for i, a := range artifacts {
- signingArtifacts[i] = signing.Artifact{Path: a.Path, OS: a.OS, Arch: a.Arch}
- }
-
- signed := signing.SignBinaries(ctx, filesystem, signCfg, signingArtifacts)
- if !signed.OK {
- if !req.CIMode {
- cli.Print("%s %s: %v\n", buildErrorStyle.Render("error"), "signing failed", signed.Error())
- }
- return signed
- }
-
- if runtime.GOOS == "darwin" && signCfg.MacOS.Notarize {
- notarized := signing.NotarizeBinaries(ctx, filesystem, signCfg, signingArtifacts)
- if !notarized.OK {
- if !req.CIMode {
- cli.Print("%s %s: %v\n", buildErrorStyle.Render("error"), "notarization failed", notarized.Error())
- }
- return notarized
- }
- }
- }
-
- // Archive artifacts if enabled
- var archivedArtifacts []build.Artifact
- if req.ArchiveOutput && len(artifacts) > 0 {
- if req.Verbose && !req.CIMode {
- cli.Blank()
- cli.Print("%s %s\n", buildHeaderStyle.Render("Archive"), "Creating archives")
- }
-
- archiveFormatResult := resolveArchiveFormat(buildConfig.Build.ArchiveFormat, req.ArchiveFormat)
- if !archiveFormatResult.OK {
- return archiveFormatResult
- }
- archiveFormatValue := archiveFormatResult.Value.(build.ArchiveFormat)
-
- archivedArtifactsResult := build.ArchiveAllWithFormat(filesystem, artifacts, archiveFormatValue)
- if !archivedArtifactsResult.OK {
- if !req.CIMode {
- cli.Print("%s %s: %v\n", buildErrorStyle.Render("error"), "archive failed", archivedArtifactsResult.Error())
- }
- return archivedArtifactsResult
- }
- archivedArtifacts = archivedArtifactsResult.Value.([]build.Artifact)
-
- if req.Verbose && !req.CIMode {
- for _, artifact := range archivedArtifacts {
- relPath := artifact.Path
- relPathResult := ax.Rel(projectDir, artifact.Path)
- if relPathResult.OK {
- relPath = relPathResult.Value.(string)
- }
- cli.Print(" %s %s %s\n",
- buildSuccessStyle.Render("*"),
- buildTargetStyle.Render(relPath),
- buildDimStyle.Render(core.Sprintf("(%s/%s)", artifact.OS, artifact.Arch)),
- )
- }
- }
- }
-
- // Compute checksums if enabled
- var checksummedArtifacts []build.Artifact
- if req.ChecksumOutput && len(archivedArtifacts) > 0 {
- checksummed := computeAndWriteChecksums(ctx, filesystem, projectDir, plan.OutputDir, archivedArtifacts, signCfg, req.CIMode, req.Verbose)
- if !checksummed.OK {
- return checksummed
- }
- checksummedArtifacts = checksummed.Value.([]build.Artifact)
- } else if req.ChecksumOutput && len(artifacts) > 0 && !req.ArchiveOutput {
- // Checksum raw binaries if archiving is disabled
- checksummed := computeAndWriteChecksums(ctx, filesystem, projectDir, plan.OutputDir, artifacts, signCfg, req.CIMode, req.Verbose)
- if !checksummed.OK {
- return checksummed
- }
- checksummedArtifacts = checksummed.Value.([]build.Artifact)
- }
-
- // Output results
- if req.CIMode {
- // Determine which artifacts to output (prefer checksummed > archived > raw).
- outputArtifacts := selectOutputArtifacts(artifacts, archivedArtifacts, checksummedArtifacts)
- metadataWritten := writeArtifactMetadata(filesystem, plan.BuildName, outputArtifacts)
- if !metadataWritten.OK {
- return metadataWritten
- }
-
- // JSON output for CI
- output := ax.JSONMarshal(outputArtifacts)
- if !output.OK {
- return core.Fail(core.E("build.Run", "failed to marshal artifacts", core.NewError(output.Error())))
- }
- cli.Print("%s\n", output.Value.(string))
- } else if !req.Verbose {
- // Minimal output: just success with artifact count
- cli.Print("%s %s %s\n",
- buildSuccessStyle.Render("Success"),
- core.Sprintf("Built %d artifacts", len(artifacts)),
- buildDimStyle.Render(core.Sprintf("(%s)", plan.OutputDir)),
- )
- }
-
- return core.Ok(nil)
-}
-
-func resolveBuildSignConfig(base signing.SignConfig, req ProjectBuildRequest) signing.SignConfig {
- signCfg := base
-
- if req.Notarize {
- signCfg.MacOS.Notarize = true
- if !req.NoSign {
- signCfg.Enabled = true
- }
- }
- if req.NoSign {
- signCfg.Enabled = false
- }
-
- return signCfg
-}
-
-func shouldUseGoBuildPassthrough(filesystem storage.Medium, projectDir string, req ProjectBuildRequest) bool {
- if req.ConfigPath != "" || build.ConfigExists(filesystem, projectDir) {
- return false
- }
-
- if req.BuildType != "" && req.BuildType != string(build.ProjectTypeGo) {
- return false
- }
-
- if !build.IsGoProject(filesystem, projectDir) {
- return false
- }
-
- projectTypesResult := build.Discover(filesystem, projectDir)
- if !projectTypesResult.OK {
- return false
- }
- projectTypes := projectTypesResult.Value.([]build.ProjectType)
- if len(projectTypes) != 1 || projectTypes[0] != build.ProjectTypeGo {
- return false
- }
-
- if req.ObfuscateSet || req.NSISSet || req.WebView2Set || req.DenoBuildSet || req.NpmBuildSet || req.BuildCacheSet || req.SignSet || req.NoSign || req.Notarize {
- return false
- }
-
- if req.Push || req.ImageName != "" || req.Format != "" {
- return false
- }
- if req.CIMode || req.Version != "" || req.ArchiveFormat != "" {
- return false
- }
- if req.ArchiveOutputSet && req.ArchiveOutput {
- return false
- }
- if req.ChecksumOutputSet && req.ChecksumOutput {
- return false
- }
- if req.PackageSet && (req.ArchiveOutput || req.ChecksumOutput) {
- return false
- }
-
- if req.TargetsFlag == "" {
- return true
- }
-
- targetsResult := parseTargets(req.TargetsFlag)
- if !targetsResult.OK {
- return false
- }
- targets := targetsResult.Value.([]build.Target)
-
- return len(targets) == 1
-}
-
-func runGoBuildPassthrough(ctx context.Context, projectDir string, req ProjectBuildRequest) core.Result {
- args := []string{"build"}
-
- if outputPath := resolveGoPassthroughOutput(req.OutputDir, req.BuildName); outputPath != "" {
- args = append(args, "-o", outputPath)
- }
-
- if tags := parseBuildTagsFlag(req.BuildTagsFlag); len(tags) > 0 {
- args = append(args, "-tags", core.Join(",", tags...))
- }
-
- args = append(args, ".")
-
- env := []string{}
- if req.TargetsFlag != "" {
- targetsResult := parseTargets(req.TargetsFlag)
- if !targetsResult.OK {
- return targetsResult
- }
- targets := targetsResult.Value.([]build.Target)
- if len(targets) != 1 {
- return core.Fail(core.E("build.Run", "go build passthrough supports exactly one target", nil))
- }
-
- env = append(env,
- "GOOS="+targets[0].OS,
- "GOARCH="+targets[0].Arch,
- )
- }
-
- built := ax.ExecWithEnv(ctx, projectDir, env, "go", args...)
- if !built.OK {
- return core.Fail(core.E("build.Run", "go build passthrough failed", core.NewError(built.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func resolveGoPassthroughOutput(outputDir, buildName string) string {
- switch {
- case outputDir != "" && buildName != "":
- return ax.Join(outputDir, buildName)
- case outputDir != "":
- return outputDir
- default:
- return buildName
- }
-}
-
-func applyProjectBuildOverrides(cfg *build.BuildConfig, req ProjectBuildRequest) {
- if cfg == nil {
- return
- }
-
- if tags := parseBuildTagsFlag(req.BuildTagsFlag); len(tags) > 0 {
- cfg.Build.BuildTags = tags
- }
-
- if req.ObfuscateSet {
- cfg.Build.Obfuscate = req.Obfuscate
- }
- if req.NSISSet {
- cfg.Build.NSIS = req.NSIS
- }
- if req.WebView2Set {
- cfg.Build.WebView2 = req.WebView2
- }
- if req.DenoBuildSet {
- cfg.Build.DenoBuild = req.DenoBuild
- }
- if req.NpmBuildSet {
- cfg.Build.NpmBuild = req.NpmBuild
- }
- if req.BuildCacheSet {
- if req.BuildCache {
- enableDefaultBuildCache(&cfg.Build.Cache)
- } else {
- cfg.Build.Cache.Enabled = false
- }
- }
- if req.SignSet {
- cfg.Sign.Enabled = req.Sign
- }
-}
-
-func parseBuildTagsFlag(value string) []string {
- if core.Trim(value) == "" {
- return nil
- }
-
- seen := make(map[string]struct{})
- var tags []string
- for _, part := range buildTagFields(value) {
- tag := core.Trim(part)
- if tag == "" {
- continue
- }
- if _, ok := seen[tag]; ok {
- continue
- }
- seen[tag] = struct{}{}
- tags = append(tags, tag)
- }
-
- return tags
-}
-
-func buildTagFields(value string) []string {
- var fields []string
- start := -1
- for i, r := range value {
- if r == ',' || unicodeIsSpace(r) {
- if start >= 0 {
- fields = append(fields, value[start:i])
- start = -1
- }
- continue
- }
- if start < 0 {
- start = i
- }
- }
- if start >= 0 {
- fields = append(fields, value[start:])
- }
- return fields
-}
-
-func enableDefaultBuildCache(cfg *build.CacheConfig) {
- if cfg == nil {
- return
- }
-
- cfg.Enabled = true
- if cfg.Directory == "" {
- cfg.Directory = ax.Join(build.ConfigDir, "cache")
- }
- if len(cfg.Paths) == 0 {
- cfg.Paths = build.DefaultBuildCachePaths("")
- }
-}
-
-func resolveProjectBuildName(projectDir string, buildConfig *build.BuildConfig, override string) string {
- return build.ResolveBuildName(projectDir, buildConfig, override)
-}
-
-func unicodeIsSpace(r rune) bool {
- return r == ' ' || r == '\t' || r == '\n' || r == '\r'
-}
-
-// selectOutputArtifacts chooses the final artifact list for CI output.
-//
-// output := selectOutputArtifacts(rawArtifacts, archivedArtifacts, checksummedArtifacts)
-func selectOutputArtifacts(rawArtifacts, archivedArtifacts, checksummedArtifacts []build.Artifact) []build.Artifact {
- if len(checksummedArtifacts) > 0 {
- return checksummedArtifacts
- }
- if len(archivedArtifacts) > 0 {
- return archivedArtifacts
- }
- return rawArtifacts
-}
-
-// writeArtifactMetadata writes artifact_meta.json files next to built artifacts when CI metadata is available.
-func writeArtifactMetadata(filesystem storage.Medium, buildName string, artifacts []build.Artifact) core.Result {
- ci := resolveCIContext()
- if ci == nil {
- return core.Ok(nil)
- }
-
- for _, artifact := range artifacts {
- if artifact.OS == "" || artifact.Arch == "" {
- continue
- }
- metaPath := ax.Join(ax.Dir(artifact.Path), "artifact_meta.json")
- written := build.WriteArtifactMeta(filesystem, metaPath, buildName, build.Target{OS: artifact.OS, Arch: artifact.Arch}, ci)
- if !written.OK {
- return written
- }
- }
-
- return core.Ok(nil)
-}
-
-func rewriteArtifactsForCI(filesystem storage.Medium, buildName string, artifacts []build.Artifact) core.Result {
- ci := resolveCIContext()
- if ci == nil {
- return core.Ok(artifacts)
- }
-
- rewritten := make([]build.Artifact, 0, len(artifacts))
- for _, artifact := range artifacts {
- ciPath := build.CIArtifactPath(buildName, ci, artifact)
- if ciPath == "" || ciPath == artifact.Path {
- rewritten = append(rewritten, artifact)
- continue
- }
-
- created := filesystem.EnsureDir(ax.Dir(ciPath))
- if !created.OK {
- return core.Fail(core.E("build.rewriteArtifactsForCI", "failed to create artifact directory", core.NewError(created.Error())))
- }
- copied := storage.Copy(filesystem, artifact.Path, filesystem, ciPath)
- if !copied.OK {
- return core.Fail(core.E("build.rewriteArtifactsForCI", "failed to copy artifact", core.NewError(copied.Error())))
- }
-
- artifact.Path = ciPath
- rewritten = append(rewritten, artifact)
- }
-
- return core.Ok(rewritten)
-}
-
-func resolveCIContext() *build.CIContext {
- if ci := build.DetectCI(); ci != nil {
- return ci
- }
-
- return build.DetectGitHubMetadata()
-}
-
-// buildRuntimeConfig maps persisted build configuration onto the runtime builder config.
-func buildRuntimeConfig(filesystem storage.Medium, projectDir, outputDir, binaryName string, buildConfig *build.BuildConfig, push bool, imageName string, version string) *build.Config {
- return build.RuntimeConfigFromBuildConfig(filesystem, projectDir, outputDir, binaryName, buildConfig, push, imageName, version)
-}
-
-// resolveArchiveFormat selects the archive format from CLI overrides or config defaults.
-func resolveArchiveFormat(configFormat, cliFormat string) core.Result {
- if cliFormat != "" {
- return build.ParseArchiveFormat(cliFormat)
- }
- return build.ParseArchiveFormat(configFormat)
-}
-
-// resolveBuildVersion determines the version string embedded into build artifacts.
-//
-// version, err := resolveBuildVersion(ctx, ".")
-func resolveBuildVersion(ctx context.Context, projectDir string) core.Result {
- return release.DetermineVersionWithContext(ctx, projectDir)
-}
-
-// computeAndWriteChecksums computes checksums for artifacts and writes CHECKSUMS.txt.
-func computeAndWriteChecksums(ctx context.Context, filesystem storage.Medium, projectDir, outputDir string, artifacts []build.Artifact, signCfg signing.SignConfig, ciMode bool, verbose bool) core.Result {
- if verbose && !ciMode {
- cli.Blank()
- cli.Print("%s %s\n", buildHeaderStyle.Render("Checksum"), "Computing checksums")
- }
-
- checksummedArtifactsResult := build.ChecksumAll(filesystem, artifacts)
- if !checksummedArtifactsResult.OK {
- if !ciMode {
- cli.Print("%s %s: %v\n", buildErrorStyle.Render("error"), "checksum failed", checksummedArtifactsResult.Error())
- }
- return checksummedArtifactsResult
- }
- checksummedArtifacts := checksummedArtifactsResult.Value.([]build.Artifact)
-
- // Write CHECKSUMS.txt
- checksumPath := ax.Join(outputDir, "CHECKSUMS.txt")
- written := build.WriteChecksumFile(filesystem, checksummedArtifacts, checksumPath)
- if !written.OK {
- if !ciMode {
- cli.Print("%s %s: %v\n", buildErrorStyle.Render("error"), "failed to write CHECKSUMS.txt", written.Error())
- }
- return written
- }
-
- // Sign checksums with GPG
- if signCfg.Enabled {
- signed := signing.SignChecksums(ctx, filesystem, signCfg, checksumPath)
- if !signed.OK {
- if !ciMode {
- cli.Print("%s %s: %v\n", buildErrorStyle.Render("error"), "GPG signing failed", signed.Error())
- }
- return signed
- }
- }
-
- if verbose && !ciMode {
- for _, artifact := range checksummedArtifacts {
- relPath := artifact.Path
- relPathResult := ax.Rel(projectDir, artifact.Path)
- if relPathResult.OK {
- relPath = relPathResult.Value.(string)
- }
- cli.Print(" %s %s\n",
- buildSuccessStyle.Render("*"),
- buildTargetStyle.Render(relPath),
- )
- cli.Print(" %s\n", buildDimStyle.Render(artifact.Checksum))
- }
-
- relChecksumPath := checksumPath
- relChecksumPathResult := ax.Rel(projectDir, checksumPath)
- if relChecksumPathResult.OK {
- relChecksumPath = relChecksumPathResult.Value.(string)
- }
- cli.Print(" %s %s\n",
- buildSuccessStyle.Render("*"),
- buildTargetStyle.Render(relChecksumPath),
- )
-
- signaturePath := checksumPath + ".asc"
- if filesystem.Exists(signaturePath) {
- relSignaturePath := signaturePath
- relSignaturePathResult := ax.Rel(projectDir, signaturePath)
- if relSignaturePathResult.OK {
- relSignaturePath = relSignaturePathResult.Value.(string)
- }
- cli.Print(" %s %s\n",
- buildSuccessStyle.Render("*"),
- buildTargetStyle.Render(relSignaturePath),
- )
- }
- }
-
- outputArtifacts := append([]build.Artifact(nil), checksummedArtifacts...)
- outputArtifacts = append(outputArtifacts, build.Artifact{Path: checksumPath})
-
- signaturePath := checksumPath + ".asc"
- if filesystem.Exists(signaturePath) {
- outputArtifacts = append(outputArtifacts, build.Artifact{Path: signaturePath})
- }
-
- return core.Ok(outputArtifacts)
-}
-
-// parseTargets parses a comma-separated list of OS/arch pairs.
-func parseTargets(targetsFlag string) core.Result {
- parts := core.Split(targetsFlag, ",")
- var targets []build.Target
-
- for _, part := range parts {
- part = core.Trim(part)
- if part == "" {
- continue
- }
-
- osArch := core.Split(part, "/")
- if len(osArch) != 2 {
- return core.Fail(core.E("build.parseTargets", "invalid target format (expected os/arch): "+part, nil))
- }
-
- targets = append(targets, build.Target{
- OS: core.Trim(osArch[0]),
- Arch: core.Trim(osArch[1]),
- })
- }
-
- if len(targets) == 0 {
- return core.Fail(core.E("build.parseTargets", "no valid targets specified", nil))
- }
-
- return core.Ok(targets)
-}
-
-// formatTargets returns a human-readable string of targets.
-func formatTargets(targets []build.Target) string {
- var parts []string
- for _, t := range targets {
- parts = append(parts, t.String())
- }
- return core.Join(", ", parts...)
-}
-
-func formatProjectTypes(projectTypes []build.ProjectType) string {
- if len(projectTypes) == 0 {
- return ""
- }
-
- parts := make([]string, 0, len(projectTypes))
- for _, projectType := range projectTypes {
- parts = append(parts, string(projectType))
- }
-
- return core.Join(", ", parts...)
-}
-
-// getBuilder returns the appropriate builder for the project type.
-func getBuilder(projectType build.ProjectType) core.Result {
- builder := builders.ResolveBuilder(projectType)
- if !builder.OK {
- return core.Fail(core.E("build.getBuilder", "unsupported project type: "+string(projectType), core.NewError(builder.Error())))
- }
- return builder
-}
diff --git a/cmd/build/cmd_project_example_test.go b/cmd/build/cmd_project_example_test.go
deleted file mode 100644
index c29da24..0000000
--- a/cmd/build/cmd_project_example_test.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package buildcmd
-
-import core "dappco.re/go"
-
-// ExampleProjectBuildRequest shows the ProjectBuildRequest type in the local build API.
-func ExampleProjectBuildRequest() {
- var value ProjectBuildRequest
- _ = value
- core.Println("ProjectBuildRequest")
- // Output: ProjectBuildRequest
-}
diff --git a/cmd/build/cmd_project_test.go b/cmd/build/cmd_project_test.go
deleted file mode 100644
index 48b5d3f..0000000
--- a/cmd/build/cmd_project_test.go
+++ /dev/null
@@ -1,964 +0,0 @@
-package buildcmd
-
-import (
- "context"
- core "dappco.re/go"
- "runtime"
- "testing"
-
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-const cmdProjectOSField = "o" + "s"
-
-func runGit(t *testing.T, dir string, args ...string) {
- t.Helper()
- requireBuildCmdOK(t, ax.ExecDir(context.Background(), dir, "git", args...))
-
-}
-
-func setupFakeGPG(t *testing.T, binDir string) {
- t.Helper()
-
- script := `#!/bin/sh
-set -eu
-
-output=""
-while [ $# -gt 0 ]; do
- case "$1" in
- --output)
- shift
- output="${1:-}"
- ;;
- esac
- shift
-done
-
-: "${output:?missing --output}"
-mkdir -p "$(dirname "$output")"
-printf 'signature\n' > "$output"
-`
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(binDir, "gpg"), []byte(script), 0o755))
-
-}
-
-func TestBuildCmd_GetBuilderGood(t *testing.T) {
- t.Run("returns Python builder for python project type", func(t *testing.T) {
- builder := requireBuildCmdBuilder(t, getBuilder(build.ProjectTypePython))
- if stdlibAssertNil(builder) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("python", builder.Name()) {
- t.Fatalf("want %v, got %v", "python", builder.Name())
- }
-
- })
-}
-
-func TestBuildCmd_buildRuntimeConfig_Good(t *testing.T) {
- buildConfig := &build.BuildConfig{
- Project: build.Project{
- Name: "sample",
- },
- Build: build.Build{
- LDFlags: []string{"-s", "-w"},
- Flags: []string{"-trimpath"},
- BuildTags: []string{"integration"},
- Env: []string{"FOO=bar"},
- CGO: true,
- Obfuscate: true,
- DenoBuild: "deno task bundle",
- NSIS: true,
- WebView2: "embed",
- Dockerfile: "Dockerfile.custom",
- Registry: "ghcr.io",
- Image: "owner/repo",
- Tags: []string{"latest", "{{.Version}}"},
- BuildArgs: map[string]string{"VERSION": "{{.Version}}"},
- Push: true,
- Load: true,
- LinuxKitConfig: ".core/linuxkit/server.yml",
- Formats: []string{"iso", "qcow2"},
- },
- }
-
- cfg := buildRuntimeConfig(storage.Local, "/project", "/project/dist", "binary", buildConfig, false, "", "v1.2.3")
- if !stdlibAssertEqual([]string{"-s", "-w"}, cfg.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w"}, cfg.LDFlags)
- }
- if !stdlibAssertEqual([]string{"-trimpath"}, cfg.Flags) {
- t.Fatalf("want %v, got %v", []string{"-trimpath"}, cfg.Flags)
- }
- if !stdlibAssertEqual([]string{"integration"}, cfg.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"integration"}, cfg.BuildTags)
- }
- if !stdlibAssertEqual([]string{"FOO=bar"}, cfg.Env) {
- t.Fatalf("want %v, got %v", []string{"FOO=bar"}, cfg.Env)
- }
- if !(cfg.CGO) {
- t.Fatal("expected true")
- }
- if !(cfg.Obfuscate) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("deno task bundle", cfg.DenoBuild) {
- t.Fatalf("want %v, got %v", "deno task bundle", cfg.DenoBuild)
- }
- if !(cfg.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("embed", cfg.WebView2) {
- t.Fatalf("want %v, got %v", "embed", cfg.WebView2)
- }
- if !stdlibAssertEqual("Dockerfile.custom", cfg.Dockerfile) {
- t.Fatalf("want %v, got %v", "Dockerfile.custom", cfg.Dockerfile)
- }
- if !stdlibAssertEqual("ghcr.io", cfg.Registry) {
- t.Fatalf("want %v, got %v", "ghcr.io", cfg.Registry)
- }
- if !stdlibAssertEqual("owner/repo", cfg.Image) {
- t.Fatalf("want %v, got %v", "owner/repo", cfg.Image)
- }
- if !stdlibAssertEqual([]string{"latest", "{{.Version}}"}, cfg.Tags) {
- t.Fatalf("want %v, got %v", []string{"latest", "{{.Version}}"}, cfg.Tags)
- }
- if !stdlibAssertEqual(map[string]string{"VERSION": "{{.Version}}"}, cfg.BuildArgs) {
- t.Fatalf("want %v, got %v", map[string]string{"VERSION": "{{.Version}}"}, cfg.BuildArgs)
- }
- if !(cfg.Push) {
- t.Fatal("expected true")
- }
- if !(cfg.Load) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(".core/linuxkit/server.yml", cfg.LinuxKitConfig) {
- t.Fatalf("want %v, got %v", ".core/linuxkit/server.yml", cfg.LinuxKitConfig)
- }
- if !stdlibAssertEqual([]string{"iso", "qcow2"}, cfg.Formats) {
- t.Fatalf("want %v, got %v", []string{"iso", "qcow2"}, cfg.Formats)
- }
- if !stdlibAssertEqual("v1.2.3", cfg.Version) {
- t.Fatalf("want %v, got %v", "v1.2.3", cfg.Version)
- }
-
-}
-
-func TestBuildCmd_buildRuntimeConfig_ImageOverride_Good(t *testing.T) {
- buildConfig := &build.BuildConfig{
- Build: build.Build{
- Image: "owner/repo",
- },
- }
-
- cfg := buildRuntimeConfig(storage.Local, "/project", "/project/dist", "binary", buildConfig, true, "cli/image", "v2.0.0")
- if !stdlibAssertEqual("cli/image", cfg.Image) {
- t.Fatalf("want %v, got %v", "cli/image", cfg.Image)
- }
- if !(cfg.Push) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("v2.0.0", cfg.Version) {
- t.Fatalf("want %v, got %v", "v2.0.0", cfg.Version)
- }
-
-}
-
-func TestBuildCmd_buildRuntimeConfig_ClonesBuildArgs_Good(t *testing.T) {
- buildConfig := &build.BuildConfig{
- Build: build.Build{
- BuildArgs: map[string]string{"VERSION": "v1.2.3"},
- },
- }
-
- cfg := buildRuntimeConfig(storage.Local, "/project", "/project/dist", "binary", buildConfig, false, "", "v1.2.3")
- if stdlibAssertNil(cfg.BuildArgs) {
- t.Fatal("expected non-nil")
- }
-
- cfg.BuildArgs["VERSION"] = "mutated"
- if !stdlibAssertEqual("v1.2.3", buildConfig.Build.BuildArgs["VERSION"]) {
- t.Fatalf("want %v, got %v", "v1.2.3", buildConfig.Build.BuildArgs["VERSION"])
- }
-
-}
-
-func TestBuildCmd_resolveNoSign_Good(t *testing.T) {
- t.Run("keeps signing enabled by default", func(t *testing.T) {
- if resolveNoSign(false, true, false) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("disables signing when no-sign is set", func(t *testing.T) {
- if !(resolveNoSign(true, true, false)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("disables signing when sign=false is set", func(t *testing.T) {
- if !(resolveNoSign(false, false, true)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("keeps signing enabled when sign=true is set", func(t *testing.T) {
- if resolveNoSign(false, true, true) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestBuildCmd_resolveBuildSignConfig_Good(t *testing.T) {
- t.Run("enables signing when notarize overrides disabled config", func(t *testing.T) {
- signCfg := resolveBuildSignConfig(build.DefaultConfig().Sign, ProjectBuildRequest{
- Notarize: true,
- })
- if !(signCfg.Enabled) {
- t.Fatal("expected true")
- }
- if !(signCfg.MacOS.Notarize) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("preserves explicit no-sign over notarize", func(t *testing.T) {
- signCfg := resolveBuildSignConfig(build.DefaultConfig().Sign, ProjectBuildRequest{
- NoSign: true,
- Notarize: true,
- })
- if signCfg.Enabled {
- t.Fatal("expected false")
- }
- if !(signCfg.MacOS.Notarize) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("re-enables signing when config disabled but notarize requested", func(t *testing.T) {
- base := build.DefaultConfig().Sign
- base.Enabled = false
-
- signCfg := resolveBuildSignConfig(base, ProjectBuildRequest{
- Notarize: true,
- })
- if !(signCfg.Enabled) {
- t.Fatal("expected true")
- }
- if !(signCfg.MacOS.Notarize) {
- t.Fatal("expected true")
- }
-
- })
-}
-
-func TestBuildCmd_resolvePackageOutputs_Good(t *testing.T) {
- t.Run("leaves archive and checksum defaults alone when package is unset", func(t *testing.T) {
- archiveOutput, checksumOutput := resolvePackageOutputs(false, false, false, false, false, false)
- if archiveOutput {
- t.Fatal("expected false")
- }
- if checksumOutput {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("disables archive and checksum when package=false and neither output flag is explicit", func(t *testing.T) {
- archiveOutput, checksumOutput := resolvePackageOutputs(false, true, true, false, true, false)
- if archiveOutput {
- t.Fatal("expected false")
- }
- if checksumOutput {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("enables archive and checksum when package=true and neither output flag is explicit", func(t *testing.T) {
- archiveOutput, checksumOutput := resolvePackageOutputs(true, true, false, false, false, false)
- if !(archiveOutput) {
- t.Fatal("expected true")
- }
- if !(checksumOutput) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("preserves explicit archive and checksum overrides over package=false", func(t *testing.T) {
- archiveOutput, checksumOutput := resolvePackageOutputs(false, true, true, true, false, true)
- if !(archiveOutput) {
- t.Fatal("expected true")
- }
- if checksumOutput {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestBuildCmd_runProjectBuild_CIModeEmitsGitHubAnnotationOnError_Bad(t *testing.T) {
- projectDir := t.TempDir()
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
-
- originalGetwd := getProjectBuildWorkingDir
- t.Cleanup(func() {
- getProjectBuildWorkingDir = originalGetwd
- cli.SetStdout(nil)
- cli.SetStderr(nil)
- })
- getProjectBuildWorkingDir = func() core.Result { return core.Ok(projectDir) }
-
- stdout := core.NewBuffer()
- cli.SetStdout(stdout)
- cli.SetStderr(stdout)
-
- result := runProjectBuild(ProjectBuildRequest{
- Context: context.Background(),
- CIMode: true,
- TargetsFlag: "linux",
- })
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(stdout.String(), emitCIAnnotationForTest(result)) {
- t.Fatalf("expected %v to contain %v", stdout.String(), emitCIAnnotationForTest(result))
- }
-
-}
-
-func TestBuildCmd_applyProjectBuildOverrides_Good(t *testing.T) {
- t.Run("applies action-style build overrides and enables default cache", func(t *testing.T) {
- cfg := build.DefaultConfig()
-
- applyProjectBuildOverrides(cfg, ProjectBuildRequest{
- BuildTagsFlag: "mlx, debug release,mlx",
- Obfuscate: true,
- ObfuscateSet: true,
- NSIS: true,
- NSISSet: true,
- WebView2: "download",
- WebView2Set: true,
- DenoBuild: "deno task bundle",
- DenoBuildSet: true,
- BuildCache: true,
- BuildCacheSet: true,
- Sign: false,
- SignSet: true,
- })
- if !stdlibAssertEqual([]string{"mlx", "debug", "release"}, cfg.Build.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"mlx", "debug", "release"}, cfg.Build.BuildTags)
- }
- if !(cfg.Build.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(cfg.Build.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("download", cfg.Build.WebView2) {
- t.Fatalf("want %v, got %v", "download", cfg.Build.WebView2)
- }
- if !stdlibAssertEqual("deno task bundle", cfg.Build.DenoBuild) {
- t.Fatalf("want %v, got %v", "deno task bundle", cfg.Build.DenoBuild)
- }
- if !(cfg.Build.Cache.Enabled) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(ax.Join(build.ConfigDir, "cache"), cfg.Build.Cache.Directory) {
- t.Fatalf("want %v, got %v", ax.Join(build.ConfigDir, "cache"), cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEqual([]string{ax.Join("cache", "go-build"), ax.Join("cache", "go-mod")}, cfg.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{ax.Join("cache", "go-build"), ax.Join("cache", "go-mod")}, cfg.Build.Cache.Paths)
- }
- if cfg.Sign.Enabled {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("preserves configured cache paths when enabling cache from the CLI", func(t *testing.T) {
- cfg := build.DefaultConfig()
- cfg.Build.Cache = build.CacheConfig{
- Directory: "custom/cache",
- Paths: []string{"custom/go-build"},
- }
-
- applyProjectBuildOverrides(cfg, ProjectBuildRequest{
- BuildCache: true,
- BuildCacheSet: true,
- })
- if !(cfg.Build.Cache.Enabled) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("custom/cache", cfg.Build.Cache.Directory) {
- t.Fatalf("want %v, got %v", "custom/cache", cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEqual([]string{"custom/go-build"}, cfg.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{"custom/go-build"}, cfg.Build.Cache.Paths)
- }
-
- })
-
- t.Run("can disable build cache without discarding the configured paths", func(t *testing.T) {
- cfg := build.DefaultConfig()
- cfg.Build.Cache = build.CacheConfig{
- Enabled: true,
- Directory: "custom/cache",
- Paths: []string{"custom/go-build", "custom/go-mod"},
- }
-
- applyProjectBuildOverrides(cfg, ProjectBuildRequest{
- BuildCache: false,
- BuildCacheSet: true,
- })
- if cfg.Build.Cache.Enabled {
- t.Fatal("expected false")
- }
- if !stdlibAssertEqual("custom/cache", cfg.Build.Cache.Directory) {
- t.Fatalf("want %v, got %v", "custom/cache", cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEqual([]string{"custom/go-build", "custom/go-mod"}, cfg.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{"custom/go-build", "custom/go-mod"}, cfg.Build.Cache.Paths)
- }
-
- })
-
- t.Run("can force signing back on when config disabled it", func(t *testing.T) {
- cfg := build.DefaultConfig()
- cfg.Sign.Enabled = false
-
- applyProjectBuildOverrides(cfg, ProjectBuildRequest{
- Sign: true,
- SignSet: true,
- })
- if !(cfg.Sign.Enabled) {
- t.Fatal("expected true")
- }
-
- })
-}
-
-func TestBuildCmd_resolveProjectBuildName_Good(t *testing.T) {
- t.Run("prefers the CLI build name override", func(t *testing.T) {
- cfg := &build.BuildConfig{
- Project: build.Project{
- Name: "project-name",
- Binary: "project-binary",
- },
- }
- if !stdlibAssertEqual("cli-name", resolveProjectBuildName("/tmp/project", cfg, "cli-name")) {
- t.Fatalf("want %v, got %v", "cli-name", resolveProjectBuildName("/tmp/project", cfg, "cli-name"))
- }
-
- })
-
- t.Run("falls back to project binary, then project name, then directory name", func(t *testing.T) {
- cfg := &build.BuildConfig{
- Project: build.Project{
- Name: "project-name",
- Binary: "project-binary",
- },
- }
- if !stdlibAssertEqual("project-binary", resolveProjectBuildName("/tmp/project", cfg, "")) {
- t.Fatalf("want %v, got %v", "project-binary", resolveProjectBuildName("/tmp/project", cfg, ""))
- }
-
- cfg.Project.Binary = ""
- if !stdlibAssertEqual("project-name", resolveProjectBuildName("/tmp/project", cfg, "")) {
- t.Fatalf("want %v, got %v", "project-name", resolveProjectBuildName("/tmp/project", cfg, ""))
- }
-
- cfg.Project.Name = ""
- if !stdlibAssertEqual("project", resolveProjectBuildName("/tmp/project", cfg, "")) {
- t.Fatalf("want %v, got %v", "project", resolveProjectBuildName("/tmp/project", cfg, ""))
- }
-
- })
-}
-
-func TestBuildCmd_resolveArchiveFormat_Good(t *testing.T) {
- t.Run("uses cli override when present", func(t *testing.T) {
- format := requireBuildCmdArchiveFormat(t, resolveArchiveFormat("gz", "xz"))
- if !stdlibAssertEqual(build.ArchiveFormatXZ, format) {
- t.Fatalf("want %v, got %v", build.ArchiveFormatXZ, format)
- }
-
- })
-
- t.Run("falls back to config when cli override is empty", func(t *testing.T) {
- format := requireBuildCmdArchiveFormat(t, resolveArchiveFormat("zip", ""))
- if !stdlibAssertEqual(build.ArchiveFormatZip, format) {
- t.Fatalf("want %v, got %v", build.ArchiveFormatZip, format)
- }
-
- })
-}
-
-func TestBuildCmd_resolveBuildVersion_Good(t *testing.T) {
- dir := t.TempDir()
-
- runGit(t, dir, "init")
- runGit(t, dir, "config", "user.email", "test@example.com")
- runGit(t, dir, "config", "user.name", "Test User")
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(dir, "README.md"), []byte("hello\n"), 0644))
-
- runGit(t, dir, "add", ".")
- runGit(t, dir, "commit", "-m", "feat: initial commit")
- runGit(t, dir, "tag", "v1.4.2")
-
- version := requireBuildCmdString(t, resolveBuildVersion(context.Background(), dir))
- if !stdlibAssertEqual("v1.4.2", version) {
- t.Fatalf("want %v, got %v", "v1.4.2", version)
- }
-
-}
-
-func TestBuildCmd_writeArtifactMetadata_Good(t *testing.T) {
- t.Setenv("GITHUB_SHA", "abc1234def5678")
- t.Setenv("GITHUB_REF", "refs/tags/v1.2.3")
- t.Setenv("GITHUB_REPOSITORY", "owner/repo")
-
- fs := storage.Local
- dir := t.TempDir()
-
- linuxDir := ax.Join(dir, "linux_amd64")
- windowsDir := ax.Join(dir, "windows_amd64")
- requireBuildCmdOK(t, ax.MkdirAll(linuxDir, 0755))
- requireBuildCmdOK(t, ax.MkdirAll(windowsDir, 0755))
-
- artifacts := []build.Artifact{
- {Path: ax.Join(linuxDir, "sample"), OS: "linux", Arch: "amd64"},
- {Path: ax.Join(windowsDir, "sample.exe"), OS: "windows", Arch: "amd64"},
- }
-
- requireBuildCmdOK(t, writeArtifactMetadata(fs, "sample", artifacts))
-
- verifyArtifactMeta := func(path string, expectedOS string, expectedArch string) {
- content := requireBuildCmdBytes(t, ax.ReadFile(path))
-
- var meta map[string]any
- requireBuildCmdOK(t, ax.JSONUnmarshal(content, &meta))
- if !stdlibAssertEqual("sample", meta["name"]) {
- t.Fatalf("want %v, got %v", "sample", meta["name"])
- }
- if !stdlibAssertEqual(expectedOS, meta[cmdProjectOSField]) {
- t.Fatalf("want %v, got %v", expectedOS, meta[cmdProjectOSField])
- }
- if !stdlibAssertEqual(expectedArch, meta["arch"]) {
- t.Fatalf("want %v, got %v", expectedArch, meta["arch"])
- }
- if !stdlibAssertEqual("v1.2.3", meta["tag"]) {
- t.Fatalf("want %v, got %v", "v1.2.3", meta["tag"])
- }
- if !stdlibAssertEqual("owner/repo", meta["repo"]) {
- t.Fatalf("want %v, got %v", "owner/repo", meta["repo"])
- }
-
- }
-
- verifyArtifactMeta(ax.Join(linuxDir, "artifact_meta.json"), "linux", "amd64")
- verifyArtifactMeta(ax.Join(windowsDir, "artifact_meta.json"), "windows", "amd64")
-}
-
-func TestBuildCmd_writeArtifactMetadata_SkipsChecksumArtifacts_Good(t *testing.T) {
- t.Setenv("GITHUB_SHA", "abc1234def5678")
- t.Setenv("GITHUB_REF", "refs/tags/v1.2.3")
- t.Setenv("GITHUB_REPOSITORY", "owner/repo")
-
- fs := storage.Local
- dir := t.TempDir()
- distDir := ax.Join(dir, "dist")
- requireBuildCmdOK(t, ax.MkdirAll(distDir, 0o755))
-
- checksumPath := ax.Join(distDir, "CHECKSUMS.txt")
- signaturePath := checksumPath + ".asc"
- requireBuildCmdOK(t, ax.WriteFile(checksumPath, []byte("checksums"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(signaturePath, []byte("signature"), 0o644))
-
- requireBuildCmdOK(t, writeArtifactMetadata(fs, "sample", []build.Artifact{
- {Path: checksumPath},
- {Path: signaturePath},
- }))
- if ax.Exists(ax.Join(distDir, "artifact_meta.json")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(distDir, "artifact_meta.json"))
- }
-
-}
-
-func TestBuildCmd_computeAndWriteChecksums_IncludesChecksumArtifacts_Good(t *testing.T) {
- projectDir := t.TempDir()
- outputDir := ax.Join(projectDir, "dist")
- artifactPath := ax.Join(outputDir, "sample_linux_amd64.tar.gz")
- requireBuildCmdOK(t, ax.MkdirAll(outputDir, 0o755))
- requireBuildCmdOK(t, ax.WriteFile(artifactPath, []byte("archive"), 0o644))
-
- signCfg := build.DefaultConfig().Sign
- signCfg.Enabled = false
-
- artifacts := requireBuildCmdArtifacts(t, computeAndWriteChecksums(
- context.Background(),
- storage.Local,
- projectDir,
- outputDir,
- []build.Artifact{{Path: artifactPath, OS: "linux", Arch: "amd64"}},
- signCfg,
- false,
- false,
- ))
-
- paths := make([]string, 0, len(artifacts))
- for _, artifact := range artifacts {
- paths = append(paths, artifact.Path)
- }
- if !stdlibAssertContains(paths, artifactPath) {
- t.Fatalf("expected %v to contain %v", paths, artifactPath)
- }
- if !stdlibAssertContains(paths, ax.Join(outputDir, "CHECKSUMS.txt")) {
- t.Fatalf("expected %v to contain %v", paths, ax.Join(outputDir, "CHECKSUMS.txt"))
- }
- if stdlibAssertContains(paths, ax.Join(outputDir, "CHECKSUMS.txt.asc")) {
- t.Fatalf("expected %v not to contain %v", paths, ax.Join(outputDir, "CHECKSUMS.txt.asc"))
- }
- requireBuildCmdOK(t, ax.Stat(ax.Join(outputDir, "CHECKSUMS.txt")))
-
-}
-
-func TestBuildCmd_computeAndWriteChecksums_IncludesSignatureArtifact_Good(t *testing.T) {
- binDir := t.TempDir()
- setupFakeGPG(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- outputDir := ax.Join(projectDir, "dist")
- artifactPath := ax.Join(outputDir, "sample_linux_amd64.tar.gz")
- requireBuildCmdOK(t, ax.MkdirAll(outputDir, 0o755))
- requireBuildCmdOK(t, ax.WriteFile(artifactPath, []byte("archive"), 0o644))
-
- signCfg := build.DefaultConfig().Sign
- signCfg.Enabled = true
- signCfg.GPG.Key = "ABCD1234"
-
- artifacts := requireBuildCmdArtifacts(t, computeAndWriteChecksums(
- context.Background(),
- storage.Local,
- projectDir,
- outputDir,
- []build.Artifact{{Path: artifactPath, OS: "linux", Arch: "amd64"}},
- signCfg,
- false,
- false,
- ))
-
- paths := make([]string, 0, len(artifacts))
- for _, artifact := range artifacts {
- paths = append(paths, artifact.Path)
- }
- if !stdlibAssertContains(paths, ax.Join(outputDir, "CHECKSUMS.txt")) {
- t.Fatalf("expected %v to contain %v", paths, ax.Join(outputDir, "CHECKSUMS.txt"))
- }
- if !stdlibAssertContains(paths, ax.Join(outputDir, "CHECKSUMS.txt.asc")) {
- t.Fatalf("expected %v to contain %v", paths, ax.Join(outputDir, "CHECKSUMS.txt.asc"))
- }
- requireBuildCmdOK(t, ax.Stat(ax.Join(outputDir, "CHECKSUMS.txt.asc")))
-
-}
-
-func TestBuildCmd_selectOutputArtifacts_Good(t *testing.T) {
- rawArtifacts := []build.Artifact{{Path: "dist/raw"}}
- archivedArtifacts := []build.Artifact{{Path: "dist/raw.tar.gz"}}
- checksummedArtifacts := []build.Artifact{{Path: "dist/raw.tar.gz", Checksum: "abc123"}}
-
- t.Run("prefers checksummed artifacts", func(t *testing.T) {
- selected := selectOutputArtifacts(rawArtifacts, archivedArtifacts, checksummedArtifacts)
- if !stdlibAssertEqual(checksummedArtifacts, selected) {
- t.Fatalf("want %v, got %v", checksummedArtifacts, selected)
- }
-
- })
-
- t.Run("falls back to archived artifacts", func(t *testing.T) {
- selected := selectOutputArtifacts(rawArtifacts, archivedArtifacts, nil)
- if !stdlibAssertEqual(archivedArtifacts, selected) {
- t.Fatalf("want %v, got %v", archivedArtifacts, selected)
- }
-
- })
-
- t.Run("falls back to raw artifacts", func(t *testing.T) {
- selected := selectOutputArtifacts(rawArtifacts, nil, nil)
- if !stdlibAssertEqual(rawArtifacts, selected) {
- t.Fatalf("want %v, got %v", rawArtifacts, selected)
- }
-
- })
-}
-
-func TestBuildCmd_runProjectBuild_PwaOverride_Good(t *testing.T) {
- expectedWD := requireBuildCmdString(t, ax.Getwd())
-
- original := runLocalPwaBuild
- t.Cleanup(func() {
- runLocalPwaBuild = original
- })
-
- called := false
- runLocalPwaBuild = func(ctx context.Context, projectDir string) core.Result {
- called = true
- if !stdlibAssertEqual(expectedWD, projectDir) {
- t.Fatalf("want %v, got %v", expectedWD, projectDir)
- }
-
- return core.Ok(nil)
- }
-
- requireBuildCmdOK(t, runProjectBuild(ProjectBuildRequest{
- Context: context.Background(),
- BuildType: "pwa",
- }))
- if !(called) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestBuildCmd_runProjectBuild_NoConfigGoPassthrough_Good(t *testing.T) {
- projectDir := t.TempDir()
- originalGetwd := getProjectBuildWorkingDir
- t.Cleanup(func() {
- getProjectBuildWorkingDir = originalGetwd
- })
- getProjectBuildWorkingDir = func() core.Result {
- return core.Ok(projectDir)
- }
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/passthrough\n\ngo 1.24\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644))
-
- requireBuildCmdOK(t, runProjectBuild(ProjectBuildRequest{
- Context: context.Background(),
- ArchiveOutput: true,
- }))
- requireBuildCmdOK(t, ax.Stat(ax.Join(projectDir, "passthrough")))
- if ax.Exists(ax.Join(projectDir, "dist")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(projectDir, "dist"))
- }
-
-}
-
-func TestBuildCmd_runProjectBuild_ConfiguredBuildDefaultsToRawArtifacts_Good(t *testing.T) {
- projectDir := t.TempDir()
- originalGetwd := getProjectBuildWorkingDir
- t.Cleanup(func() {
- getProjectBuildWorkingDir = originalGetwd
- })
- getProjectBuildWorkingDir = func() core.Result {
- return core.Ok(projectDir)
- }
- requireBuildCmdOK(t, ax.MkdirAll(ax.Join(projectDir, ".core"), 0o755))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/configured\n\ngo 1.24\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, ".core", "build.yaml"), []byte("version: 1\n"+"project:\n"+" name: configured\n"+" binary: configured\n"+"targets:\n"+" - os: "+runtime.GOOS+"\n"+" arch: "+runtime.GOARCH+"\n"+"sign:\n"+" enabled: false\n"), 0o644))
-
- requireBuildCmdOK(t, runProjectBuild(ProjectBuildRequest{
- Context: context.Background(),
- }))
-
- expectedBinary := ax.Join(projectDir, "dist", runtime.GOOS+"_"+runtime.GOARCH, "configured")
- if runtime.GOOS == "windows" {
- expectedBinary += ".exe"
- }
- requireBuildCmdOK(t, ax.Stat(expectedBinary))
- if ax.Exists(ax.Join(projectDir, "dist", "CHECKSUMS.txt")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(projectDir, "dist", "CHECKSUMS.txt"))
- }
- if ax.Exists(ax.Join(projectDir, "dist", "configured_"+runtime.GOOS+"_"+runtime.GOARCH+".tar.gz")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(projectDir, "dist", "configured_"+runtime.GOOS+"_"+runtime.GOARCH+".tar.gz"))
- }
- if ax.Exists(ax.Join(projectDir, "dist", "configured_"+runtime.GOOS+"_"+runtime.GOARCH+".tar.xz")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(projectDir, "dist", "configured_"+runtime.GOOS+"_"+runtime.GOARCH+".tar.xz"))
- }
- if ax.Exists(ax.Join(projectDir, "dist", "configured_"+runtime.GOOS+"_"+runtime.GOARCH+".zip")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(projectDir, "dist", "configured_"+runtime.GOOS+"_"+runtime.GOARCH+".zip"))
- }
-
-}
-
-func TestBuildCmd_shouldUseGoBuildPassthrough_Good(t *testing.T) {
- projectDir := t.TempDir()
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/passthrough\n\ngo 1.24\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644))
-
- t.Run("keeps simple no-config go builds on passthrough", func(t *testing.T) {
- if !(shouldUseGoBuildPassthrough(storage.Local, projectDir, ProjectBuildRequest{})) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("uses the pipeline for ci mode", func(t *testing.T) {
- if (shouldUseGoBuildPassthrough(storage.Local, projectDir, ProjectBuildRequest{CIMode: true})) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("uses the pipeline for explicit archive requests", func(t *testing.T) {
- if (shouldUseGoBuildPassthrough(storage.Local, projectDir, ProjectBuildRequest{ArchiveOutput: true, ArchiveOutputSet: true})) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("uses the pipeline for explicit package requests", func(t *testing.T) {
- if (shouldUseGoBuildPassthrough(storage.Local, projectDir, ProjectBuildRequest{ArchiveOutput: true, ChecksumOutput: true, PackageSet: true})) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("uses the pipeline for explicit versioning", func(t *testing.T) {
- if (shouldUseGoBuildPassthrough(storage.Local, projectDir, ProjectBuildRequest{Version: "v1.2.3"})) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("uses the pipeline for Wails projects even without config", func(t *testing.T) {
- wailsDir := t.TempDir()
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(wailsDir, "go.mod"), []byte("module example.com/wails\n\ngo 1.24\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(wailsDir, "wails.json"), []byte(`{"name":"demo"}`), 0o644))
- if (shouldUseGoBuildPassthrough(storage.Local, wailsDir, ProjectBuildRequest{})) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("uses the pipeline for multi-type Go and Node projects", func(t *testing.T) {
- stackDir := t.TempDir()
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(stackDir, "go.mod"), []byte("module example.com/fullstack\n\ngo 1.24\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(stackDir, "package.json"), []byte(`{"name":"fullstack"}`), 0o644))
- if (shouldUseGoBuildPassthrough(storage.Local, stackDir, ProjectBuildRequest{})) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestBuildCmd_runProjectBuild_NoConfigGoPassthroughTargetAndOutput_Good(t *testing.T) {
- projectDir := t.TempDir()
- outputDir := ax.Join(projectDir, "bin")
- outputPath := ax.Join(outputDir, "custom-binary")
- originalGetwd := getProjectBuildWorkingDir
- t.Cleanup(func() {
- getProjectBuildWorkingDir = originalGetwd
- })
- getProjectBuildWorkingDir = func() core.Result {
- return core.Ok(projectDir)
- }
- requireBuildCmdOK(t, ax.MkdirAll(outputDir, 0o755))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/passthrough\n\ngo 1.24\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644))
-
- requireBuildCmdOK(t, runProjectBuild(ProjectBuildRequest{
- Context: context.Background(),
- TargetsFlag: "linux/amd64",
- OutputDir: outputDir,
- BuildName: "custom-binary",
- }))
- requireBuildCmdOK(t, ax.Stat(outputPath))
-
-}
-
-func TestBuildCmd_runProjectBuild_NoConfigGoCIModeUsesPipeline_Good(t *testing.T) {
- projectDir := t.TempDir()
- originalGetwd := getProjectBuildWorkingDir
- t.Cleanup(func() {
- getProjectBuildWorkingDir = originalGetwd
- })
- getProjectBuildWorkingDir = func() core.Result {
- return core.Ok(projectDir)
- }
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/passthrough\n\ngo 1.24\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644))
-
- buildName := ax.Base(projectDir)
-
- requireBuildCmdOK(t, runProjectBuild(ProjectBuildRequest{
- Context: context.Background(),
- CIMode: true,
- TargetsFlag: "linux/amd64",
- ArchiveOutput: false,
- ChecksumOutput: false,
- }))
- if ax.Exists(ax.Join(projectDir, "passthrough")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(projectDir, "passthrough"))
- }
- requireBuildCmdOK(t, ax.Stat(ax.Join(projectDir, "dist", "linux_amd64", buildName)))
-
-}
-
-func TestBuildCmd_runProjectBuild_CIModeCopiesCIStampedArtifacts_Good(t *testing.T) {
- projectDir := t.TempDir()
- originalGetwd := getProjectBuildWorkingDir
- t.Cleanup(func() {
- getProjectBuildWorkingDir = originalGetwd
- })
- getProjectBuildWorkingDir = func() core.Result {
- return core.Ok(projectDir)
- }
-
- t.Setenv("GITHUB_SHA", "abc1234def5678901234567890123456789012345")
- t.Setenv("GITHUB_REF", "refs/tags/v1.2.3")
- t.Setenv("GITHUB_REPOSITORY", "owner/repo")
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/passthrough\n\ngo 1.24\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644))
-
- requireBuildCmdOK(t, runProjectBuild(ProjectBuildRequest{
- Context: context.Background(),
- CIMode: true,
- TargetsFlag: "linux/amd64",
- }))
-
- ciArtifactPath := ax.Join(projectDir, "dist", "linux_amd64", ax.Base(projectDir)+"_linux_amd64_v1.2.3")
- requireBuildCmdOK(t, ax.Stat(ciArtifactPath))
- requireBuildCmdOK(t, ax.Stat(ax.Join(projectDir, "dist", "linux_amd64", ax.Base(projectDir))))
- requireBuildCmdOK(t, ax.Stat(ax.Join(projectDir, "dist", "linux_amd64", "artifact_meta.json")))
-
-}
-
-func TestBuildCmd_runProjectBuild_NoConfigGoArchiveRequestUsesPipeline_Good(t *testing.T) {
- projectDir := t.TempDir()
- originalGetwd := getProjectBuildWorkingDir
- t.Cleanup(func() {
- getProjectBuildWorkingDir = originalGetwd
- })
- getProjectBuildWorkingDir = func() core.Result {
- return core.Ok(projectDir)
- }
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/passthrough\n\ngo 1.24\n"), 0o644))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644))
-
- buildName := ax.Base(projectDir)
-
- requireBuildCmdOK(t, runProjectBuild(ProjectBuildRequest{
- Context: context.Background(),
- TargetsFlag: "linux/amd64",
- ArchiveOutput: true,
- ArchiveOutputSet: true,
- ChecksumOutput: false,
- }))
- if ax.Exists(ax.Join(projectDir, "passthrough")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(projectDir, "passthrough"))
- }
- requireBuildCmdOK(t, ax.Stat(ax.Join(projectDir, "dist", "linux_amd64", buildName)))
- requireBuildCmdOK(t, ax.Stat(ax.Join(projectDir, "dist", buildName+"_linux_amd64.tar.gz")))
-
-}
diff --git a/cmd/build/cmd_pwa.go b/cmd/build/cmd_pwa.go
deleted file mode 100644
index b7b8c83..0000000
--- a/cmd/build/cmd_pwa.go
+++ /dev/null
@@ -1,814 +0,0 @@
-// cmd_pwa.go implements PWA and legacy GUI build functionality.
-//
-// Supports building desktop applications from:
-// - Local static web application directories
-// - Live PWA URLs (downloads and packages)
-
-package buildcmd
-
-import (
- // Note: AX-6 — context.Context is the command cancellation contract; core has no equivalent API.
- "context"
- "io/fs"
- // Note: AX-6 — net/http is required for PWA downloads; core has no HTTP client primitive.
- "net/http"
- // Note: AX-6 — net/url is required for standards-compliant URL parsing/resolution; core has only path/string primitives here.
- "net/url"
- // Note: AX-6 — unicode preserves Fields/slug whitespace semantics; core has no rune category primitive.
- "unicode"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "github.com/leaanthony/debme"
- "github.com/leaanthony/gosod"
- "golang.org/x/net/html"
-)
-
-// Error sentinels for build commands
-var (
- errPathRequired = core.E("buildcmd.Init", "the --path flag is required", nil)
- errURLRequired = core.E("buildcmd.Init", "the --url flag is required", nil)
- errPWAInputRequired = core.E("buildcmd.Init", "either --path or --url is required", nil)
-)
-
-// runLocalPwaBuild points at the local PWA build entrypoint.
-// Tests replace this to avoid invoking the real build toolchain.
-var runLocalPwaBuild = runBuild
-
-const defaultPWADescription = "A web application enclaved by Core."
-
-type pwaMetadata struct {
- DisplayName string
- Description string
- ManifestURL string
- Icons []string
-}
-
-type pwaAppConfig struct {
- ModuleName string
- DisplayName string
- Description string
-}
-
-type pwaHTMLExtraction struct {
- Metadata pwaMetadata
- Assets []string
-}
-
-type pwaManifestFetch struct {
- Manifest map[string]any
- Body []byte
-}
-
-// runPwaBuild downloads a PWA from URL and builds it.
-func runPwaBuild(ctx context.Context, pwaURL string) core.Result {
- core.Print(nil, "%s %s", "Building PWA", pwaURL)
-
- tempDirResult := ax.TempDir("core-pwa-build-*")
- if !tempDirResult.OK {
- return core.Fail(core.E("pwa.runPwaBuild", "failed to create temporary directory", core.NewError(tempDirResult.Error())))
- }
- tempDir := tempDirResult.Value.(string)
- // defer os.RemoveAll(tempDir) // Keep temp dir for debugging
- core.Print(nil, "%s %s", "Downloading to", tempDir)
-
- downloaded := downloadPWA(ctx, pwaURL, tempDir)
- if !downloaded.OK {
- return core.Fail(core.E("pwa.runPwaBuild", "failed to download PWA", core.NewError(downloaded.Error())))
- }
-
- return runBuild(ctx, tempDir)
-}
-
-// downloadPWA fetches a PWA from a URL and saves assets locally.
-func downloadPWA(ctx context.Context, baseURL, destDir string) core.Result {
- respResult := getWithContext(ctx, baseURL)
- if !respResult.OK {
- return core.Fail(core.E("pwa.downloadPWA", "failed to fetch URL "+baseURL, core.NewError(respResult.Error())))
- }
- resp := respResult.Value.(*http.Response)
- bodyResult := readAllBytes(resp.Body)
- if !bodyResult.OK {
- return core.Fail(core.E("pwa.downloadPWA", "failed to read response body", core.NewError(bodyResult.Error())))
- }
- body := bodyResult.Value.([]byte)
-
- extractedResult := extractHTMLMetadataAndAssets(string(body), baseURL)
- if !extractedResult.OK {
- return core.Fail(core.E("pwa.downloadPWA", "failed to parse HTML entry point", core.NewError(extractedResult.Error())))
- }
- extracted := extractedResult.Value.(pwaHTMLExtraction)
- pageMetadata := extracted.Metadata
- assets := extracted.Assets
-
- writtenIndex := ax.WriteFile(ax.Join(destDir, "index.html"), body, 0o644)
- if !writtenIndex.OK {
- return core.Fail(core.E("pwa.downloadPWA", "failed to write index.html", core.NewError(writtenIndex.Error())))
- }
-
- downloaded := map[string]struct{}{
- normalizeAssetURL(baseURL): {},
- }
-
- if pageMetadata.ManifestURL == "" {
- core.Print(nil, "%s %s", "warning", "no manifest found")
- } else {
- core.Print(nil, "%s %s", "Found manifest", pageMetadata.ManifestURL)
-
- manifestResult := fetchManifest(ctx, pageMetadata.ManifestURL)
- if !manifestResult.OK {
- return core.Fail(core.E("pwa.downloadPWA", "failed to fetch or parse manifest", core.NewError(manifestResult.Error())))
- }
- manifestFetch := manifestResult.Value.(pwaManifestFetch)
-
- manifestWritten := writeURLAsset(destDir, pageMetadata.ManifestURL, manifestFetch.Body)
- if !manifestWritten.OK {
- return core.Fail(core.E("pwa.downloadPWA", "failed to write manifest", core.NewError(manifestWritten.Error())))
- }
- downloaded[normalizeAssetURL(pageMetadata.ManifestURL)] = struct{}{}
- assets = append(assets, collectAssets(manifestFetch.Manifest, pageMetadata.ManifestURL)...)
- }
-
- for _, assetURL := range uniquePWAStrings(assets) {
- normalized := normalizeAssetURL(assetURL)
- if normalized == "" {
- continue
- }
- if _, ok := downloaded[normalized]; ok {
- continue
- }
- assetDownloaded := downloadAsset(ctx, assetURL, destDir)
- if !assetDownloaded.OK {
- if ctx.Err() != nil {
- return core.Fail(core.E("pwa.downloadPWA", "download cancelled", ctx.Err()))
- }
- core.Print(nil, "%s %s %s: %v", "warning", "failed to download asset", assetURL, assetDownloaded.Error())
- continue
- }
- downloaded[normalized] = struct{}{}
- }
-
- core.Println("PWA download complete")
- return core.Ok(nil)
-}
-
-// findManifestURL extracts the manifest URL from HTML content.
-func findManifestURL(htmlContent, baseURL string) core.Result {
- extracted := extractHTMLMetadataAndAssets(htmlContent, baseURL)
- if !extracted.OK {
- return extracted
- }
- metadata := extracted.Value.(pwaHTMLExtraction).Metadata
- if metadata.ManifestURL == "" {
- return core.Fail(core.E("pwa.findManifestURL", "manifest tag not found", nil))
- }
- return core.Ok(metadata.ManifestURL)
-}
-
-func extractHTMLMetadataAndAssets(htmlContent, baseURL string) core.Result {
- doc, err := html.Parse(core.NewReader(htmlContent))
- if err != nil {
- return core.Fail(err)
- }
-
- base, err := url.Parse(baseURL)
- if err != nil {
- return core.Fail(err)
- }
-
- var (
- metadata pwaMetadata
- assets []string
- )
-
- var walk func(*html.Node)
- walk = func(node *html.Node) {
- if node.Type == html.ElementNode {
- switch core.Lower(core.Trim(node.Data)) {
- case "title":
- if metadata.DisplayName == "" {
- metadata.DisplayName = core.Trim(nodeText(node))
- }
- case "meta":
- content := core.Trim(attributeValue(node, "content"))
- name := core.Lower(core.Trim(attributeValue(node, "name")))
- property := core.Lower(core.Trim(attributeValue(node, "property")))
- if content != "" && (name == "description" || property == "og:description" || property == "twitter:description") && metadata.Description == "" {
- metadata.Description = content
- }
- case "link":
- relValue := attributeValue(node, "rel")
- href := attributeValue(node, "href")
- rel := parseRelTokens(relValue)
- resolved := resolveAssetURL(base, href)
- if resolved != "" && relHasAny(rel, "stylesheet", "icon", "shortcut", "apple-touch-icon", "mask-icon", "preload", "modulepreload", "prefetch", "manifest") {
- assets = append(assets, resolved)
- }
- if relIncludesManifest(relValue) && resolved != "" && metadata.ManifestURL == "" {
- metadata.ManifestURL = resolved
- }
- if resolved != "" && relHasAny(rel, "icon", "apple-touch-icon", "mask-icon") {
- metadata.Icons = append(metadata.Icons, resolved)
- }
- case "script":
- appendResolvedAsset(&assets, base, attributeValue(node, "src"))
- case "img":
- appendResolvedAsset(&assets, base, attributeValue(node, "src"))
- appendResolvedSrcSet(&assets, base, attributeValue(node, "srcset"))
- case "source":
- appendResolvedAsset(&assets, base, attributeValue(node, "src"))
- appendResolvedSrcSet(&assets, base, attributeValue(node, "srcset"))
- case "video":
- appendResolvedAsset(&assets, base, attributeValue(node, "poster"))
- }
- }
-
- for child := node.FirstChild; child != nil; child = child.NextSibling {
- walk(child)
- }
- }
- walk(doc)
-
- metadata.Icons = uniquePWAStrings(metadata.Icons)
- assets = uniquePWAStrings(assets)
- return core.Ok(pwaHTMLExtraction{Metadata: metadata, Assets: assets})
-}
-
-// relIncludesManifest reports whether a rel attribute declares a manifest link.
-// HTML allows multiple space-separated tokens and case-insensitive values.
-func relIncludesManifest(rel string) bool {
- for _, token := range parseRelTokens(rel) {
- if token == "manifest" {
- return true
- }
- }
- return false
-}
-
-// fetchManifest downloads and parses a PWA manifest.
-func fetchManifest(ctx context.Context, manifestURL string) core.Result {
- respResult := getWithContext(ctx, manifestURL)
- if !respResult.OK {
- return respResult
- }
- resp := respResult.Value.(*http.Response)
- bodyResult := readAllBytes(resp.Body)
- if !bodyResult.OK {
- return bodyResult
- }
- body := bodyResult.Value.([]byte)
-
- var manifest map[string]any
- decoded := ax.JSONUnmarshal(body, &manifest)
- if !decoded.OK {
- return decoded
- }
- return core.Ok(pwaManifestFetch{Manifest: manifest, Body: body})
-}
-
-// collectAssets extracts asset URLs from a PWA manifest.
-func collectAssets(manifest map[string]any, manifestURL string) []string {
- _, assets := manifestMetadataAndAssets(manifest, manifestURL)
- return assets
-}
-
-// downloadAsset fetches a single asset and saves it locally.
-func downloadAsset(ctx context.Context, assetURL, destDir string) core.Result {
- respResult := getWithContext(ctx, assetURL)
- if !respResult.OK {
- return respResult
- }
- resp := respResult.Value.(*http.Response)
- bodyResult := readAllBytes(resp.Body)
- if !bodyResult.OK {
- return bodyResult
- }
- body := bodyResult.Value.([]byte)
-
- return writeURLAsset(destDir, assetURL, body)
-}
-
-func writeURLAsset(destDir, assetURL string, body []byte) core.Result {
- targetPathResult := resolveAssetDestination(destDir, assetURL)
- if !targetPathResult.OK {
- return targetPathResult
- }
- targetPath := targetPathResult.Value.(string)
- created := ax.MkdirAll(ax.Dir(targetPath), 0o755)
- if !created.OK {
- return created
- }
- return ax.WriteFile(targetPath, body, 0o644)
-}
-
-// runBuild builds a desktop application from a local directory.
-func runBuild(ctx context.Context, fromPath string) core.Result {
- core.Print(nil, "%s %s", "Building from path", fromPath)
-
- if !ax.IsDir(fromPath) {
- return core.Fail(core.E("pwa.runBuild", "path must be a directory", nil))
- }
-
- buildDir := ".core/build/app"
- htmlDir := ax.Join(buildDir, "html")
- appConfig := resolvePWAAppConfig(fromPath)
- outputExe := appConfig.ModuleName
-
- removed := ax.RemoveAll(buildDir)
- if !removed.OK {
- return core.Fail(core.E("pwa.runBuild", "failed to clean build directory", core.NewError(removed.Error())))
- }
-
- // 1. Generate the project from the embedded template
- core.Println("Generating template")
- templateFS, err := debme.FS(guiTemplate, "tmpl/gui")
- if err != nil {
- return core.Fail(core.E("pwa.runBuild", "failed to anchor template filesystem", err))
- }
- sod := gosod.New(templateFS)
- if sod == nil {
- return core.Fail(core.E("pwa.runBuild", "failed to create new sod instance", nil))
- }
-
- templateData := map[string]string{
- "AppModule": appConfig.ModuleName,
- "AppDisplayNameLiteral": core.Sprintf("%q", appConfig.DisplayName),
- "AppDescriptionLiteral": core.Sprintf("%q", appConfig.Description),
- }
- if err := sod.Extract(buildDir, templateData); err != nil {
- return core.Fail(core.E("pwa.runBuild", "failed to extract template", err))
- }
-
- // 2. Copy the user's web app files
- core.Println("Copying files")
- copied := copyDir(fromPath, htmlDir)
- if !copied.OK {
- return core.Fail(core.E("pwa.runBuild", "failed to copy application files", core.NewError(copied.Error())))
- }
-
- // 3. Compile the application
- core.Println("Compiling")
-
- // Run go mod tidy
- tidied := ax.ExecDir(ctx, buildDir, "go", "mod", "tidy")
- if !tidied.OK {
- return core.Fail(core.E("pwa.runBuild", "go mod tidy failed", core.NewError(tidied.Error())))
- }
-
- // Run go build
- built := ax.ExecDir(ctx, buildDir, "go", "build", "-o", outputExe)
- if !built.OK {
- return core.Fail(core.E("pwa.runBuild", "go build failed", core.NewError(built.Error())))
- }
-
- core.Println()
- core.Print(nil, "%s %s/%s", "Built", buildDir, outputExe)
- return core.Ok(nil)
-}
-
-func resolvePWAAppConfig(fromPath string) pwaAppConfig {
- fallbackName := ax.Base(fromPath)
- if core.HasPrefix(fallbackName, "core-pwa-build-") {
- fallbackName = "PWA App"
- }
-
- metadata := loadLocalPWAMetadata(fromPath)
- displayName := core.Trim(metadata.DisplayName)
- if displayName == "" {
- displayName = fallbackName
- }
-
- description := core.Trim(metadata.Description)
- if description == "" {
- description = defaultPWADescription
- }
-
- moduleName := slugifyPWAName(displayName)
- if moduleName == "" {
- moduleName = slugifyPWAName(fallbackName)
- }
- if moduleName == "" {
- moduleName = "pwa-app"
- }
-
- return pwaAppConfig{
- ModuleName: moduleName,
- DisplayName: displayName,
- Description: description,
- }
-}
-
-func loadLocalPWAMetadata(dir string) pwaMetadata {
- indexPath := ax.Join(dir, "index.html")
- if !ax.IsFile(indexPath) {
- return pwaMetadata{}
- }
-
- contentResult := ax.ReadFile(indexPath)
- if !contentResult.OK {
- return pwaMetadata{}
- }
- content := contentResult.Value.([]byte)
-
- extracted := extractHTMLMetadataAndAssets(string(content), "https://local.core/")
- if !extracted.OK {
- return pwaMetadata{}
- }
- metadata := extracted.Value.(pwaHTMLExtraction).Metadata
-
- for _, manifestPath := range localManifestCandidates(dir, metadata.ManifestURL) {
- if !ax.IsFile(manifestPath) {
- continue
- }
-
- manifestBodyResult := ax.ReadFile(manifestPath)
- if !manifestBodyResult.OK {
- continue
- }
- manifestBody := manifestBodyResult.Value.([]byte)
-
- relativePathResult := ax.Rel(dir, manifestPath)
- if !relativePathResult.OK {
- continue
- }
- relativePath := relativePathResult.Value.(string)
- manifestURL := core.Concat("https://local.core/", localPWAURLPath(relativePath))
- manifestMetadata, _ := manifestMetadataAndAssetsFromBytes(manifestBody, manifestURL)
- return mergePWAMetadata(metadata, manifestMetadata)
- }
-
- return metadata
-}
-
-func getWithContext(ctx context.Context, targetURL string) core.Result {
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
- if err != nil {
- return core.Fail(err)
- }
- return core.ResultOf(http.DefaultClient.Do(req))
-}
-
-func readAllBytes(reader any) core.Result {
- result := core.ReadAll(reader)
- if !result.OK {
- if err, ok := result.Value.(error); ok {
- return core.Fail(err)
- }
- return core.Fail(core.E("pwa.readAllBytes", "failed to read stream", nil))
- }
-
- content, ok := result.Value.(string)
- if !ok {
- return core.Fail(core.E("pwa.readAllBytes", "read stream returned non-string content", nil))
- }
- return core.Ok([]byte(content))
-}
-
-// copyDir recursively copies a directory from src to dst.
-func copyDir(src, dst string) core.Result {
- created := ax.MkdirAll(dst, 0o755)
- if !created.OK {
- return created
- }
-
- entriesResult := ax.ReadDir(src)
- if !entriesResult.OK {
- return entriesResult
- }
- entries := entriesResult.Value.([]fs.DirEntry)
-
- for _, entry := range entries {
- srcPath := ax.Join(src, entry.Name())
- dstPath := ax.Join(dst, entry.Name())
-
- if entry.IsDir() {
- copied := copyDir(srcPath, dstPath)
- if !copied.OK {
- return copied
- }
- continue
- }
-
- srcFile := ax.Open(srcPath)
- if !srcFile.OK {
- return srcFile
- }
-
- content := readAllBytes(srcFile.Value)
- if !content.OK {
- return content
- }
-
- written := ax.WriteFile(dstPath, content.Value.([]byte), 0o644)
- if !written.OK {
- return written
- }
- }
-
- return core.Ok(nil)
-}
-
-func manifestMetadataAndAssets(manifest map[string]any, manifestURL string) (pwaMetadata, []string) {
- metadata := pwaMetadata{}
- var assets []string
- base, _ := url.Parse(manifestURL)
-
- if name, ok := manifest["name"].(string); ok && core.Trim(name) != "" {
- metadata.DisplayName = core.Trim(name)
- } else if shortName, ok := manifest["short_name"].(string); ok {
- metadata.DisplayName = core.Trim(shortName)
- }
-
- if description, ok := manifest["description"].(string); ok {
- metadata.Description = core.Trim(description)
- }
-
- if startURL, ok := manifest["start_url"].(string); ok {
- appendResolvedAsset(&assets, base, startURL)
- }
-
- if icons, ok := manifest["icons"].([]any); ok {
- for _, icon := range icons {
- iconMap, ok := icon.(map[string]any)
- if !ok {
- continue
- }
- src, _ := iconMap["src"].(string)
- resolved := resolveAssetURL(base, src)
- if resolved == "" {
- continue
- }
- metadata.Icons = append(metadata.Icons, resolved)
- assets = append(assets, resolved)
- }
- }
-
- metadata.Icons = uniquePWAStrings(metadata.Icons)
- assets = uniquePWAStrings(assets)
- return metadata, assets
-}
-
-func manifestMetadataAndAssetsFromBytes(body []byte, manifestURL string) (pwaMetadata, []string) {
- var manifest map[string]any
- decoded := ax.JSONUnmarshal(body, &manifest)
- if !decoded.OK {
- return pwaMetadata{}, nil
- }
- return manifestMetadataAndAssets(manifest, manifestURL)
-}
-
-func mergePWAMetadata(base, override pwaMetadata) pwaMetadata {
- merged := base
- if core.Trim(override.DisplayName) != "" {
- merged.DisplayName = core.Trim(override.DisplayName)
- }
- if core.Trim(override.Description) != "" {
- merged.Description = core.Trim(override.Description)
- }
- if core.Trim(override.ManifestURL) != "" {
- merged.ManifestURL = core.Trim(override.ManifestURL)
- }
- merged.Icons = uniquePWAStrings(append(append([]string{}, base.Icons...), override.Icons...))
- return merged
-}
-
-func attributeValue(node *html.Node, name string) string {
- needle := core.Lower(name)
- for _, attribute := range node.Attr {
- if core.Lower(attribute.Key) == needle {
- return attribute.Val
- }
- }
- return ""
-}
-
-func nodeText(node *html.Node) string {
- b := core.NewBuilder()
- var walk func(*html.Node)
- walk = func(current *html.Node) {
- if current.Type == html.TextNode {
- b.WriteString(current.Data)
- }
- for child := current.FirstChild; child != nil; child = child.NextSibling {
- walk(child)
- }
- }
- walk(node)
- return b.String()
-}
-
-func parseRelTokens(value string) []string {
- return uniquePWAStrings(pwaFields(core.Lower(core.Trim(value))))
-}
-
-func relHasAny(tokens []string, candidates ...string) bool {
- for _, token := range tokens {
- for _, candidate := range candidates {
- if token == candidate {
- return true
- }
- }
- }
- return false
-}
-
-func resolveAssetURL(base *url.URL, raw string) string {
- raw = core.Trim(raw)
- if raw == "" || core.HasPrefix(raw, "#") {
- return ""
- }
-
- lower := core.Lower(raw)
- if core.HasPrefix(lower, "data:") || core.HasPrefix(lower, "javascript:") || core.HasPrefix(lower, "mailto:") {
- return ""
- }
-
- resolved, err := base.Parse(raw)
- if err != nil {
- return ""
- }
- if resolved.Scheme != "http" && resolved.Scheme != "https" {
- return ""
- }
- resolved.Fragment = ""
- return resolved.String()
-}
-
-func appendResolvedAsset(assets *[]string, base *url.URL, raw string) {
- resolved := resolveAssetURL(base, raw)
- if resolved != "" {
- *assets = append(*assets, resolved)
- }
-}
-
-func appendResolvedSrcSet(assets *[]string, base *url.URL, raw string) {
- for _, candidate := range core.Split(raw, ",") {
- candidate = core.Trim(candidate)
- if candidate == "" {
- continue
- }
- fields := pwaFields(candidate)
- if len(fields) == 0 {
- continue
- }
- appendResolvedAsset(assets, base, fields[0])
- }
-}
-
-func uniquePWAStrings(values []string) []string {
- if len(values) == 0 {
- return values
- }
-
- result := make([]string, 0, len(values))
- seen := make(map[string]struct{}, len(values))
- for _, value := range values {
- value = core.Trim(value)
- if value == "" {
- continue
- }
- if _, ok := seen[value]; ok {
- continue
- }
- seen[value] = struct{}{}
- result = append(result, value)
- }
- return result
-}
-
-func normalizeAssetURL(raw string) string {
- parsed, err := url.Parse(core.Trim(raw))
- if err != nil {
- return ""
- }
- parsed.Fragment = ""
- return parsed.String()
-}
-
-func resolveAssetDestination(destDir, assetURL string) core.Result {
- parsed, err := url.Parse(assetURL)
- if err != nil {
- return core.Fail(err)
- }
-
- relativePath := cleanPWAURLPath(core.Concat("/", parsed.Path))
- switch {
- case relativePath == "/" || relativePath == ".":
- relativePath = "/index.html"
- case core.HasSuffix(parsed.Path, "/"):
- relativePath = joinPWAURLPath(relativePath, "index.html")
- }
-
- return core.Ok(ax.Join(destDir, ax.FromSlash(core.TrimPrefix(relativePath, "/"))))
-}
-
-func localManifestCandidates(dir, manifestURL string) []string {
- candidates := make([]string, 0, 3)
- if manifestURL != "" {
- if localPath := localAssetPath(dir, manifestURL); localPath != "" {
- candidates = append(candidates, localPath)
- }
- }
- candidates = append(candidates, ax.Join(dir, "manifest.json"), ax.Join(dir, "manifest.webmanifest"))
- return uniquePWAStrings(candidates)
-}
-
-func localAssetPath(dir, assetURL string) string {
- parsed, err := url.Parse(assetURL)
- if err != nil {
- return ""
- }
-
- relativePath := cleanPWAURLPath(core.Concat("/", parsed.Path))
- if relativePath == "/" || relativePath == "." {
- relativePath = "/index.html"
- }
- return ax.Join(dir, ax.FromSlash(core.TrimPrefix(relativePath, "/")))
-}
-
-func slugifyPWAName(name string) string {
- name = core.Trim(name)
- if name == "" {
- return ""
- }
-
- b := core.NewBuilder()
- lastDash := false
- for _, r := range core.Lower(name) {
- switch {
- case isPWAASCIILetter(r) || isPWAASCIIDigit(r):
- b.WriteRune(r)
- lastDash = false
- case isPWASpace(r) || r == '-' || r == '_' || r == '.':
- if b.Len() == 0 || lastDash {
- continue
- }
- b.WriteByte('-')
- lastDash = true
- }
- }
-
- slug := trimPWAHyphens(b.String())
- if slug == "" {
- return ""
- }
- if slug[0] >= '0' && slug[0] <= '9' {
- return core.Concat("app-", slug)
- }
- return slug
-}
-
-func cleanPWAURLPath(value string) string {
- return core.CleanPath(value, "/")
-}
-
-func joinPWAURLPath(parts ...string) string {
- return cleanPWAURLPath(core.Join("/", parts...))
-}
-
-func localPWAURLPath(relativePath string) string {
- return core.TrimPrefix(cleanPWAURLPath(core.Concat("/", core.Replace(relativePath, ax.DS(), "/"))), "/")
-}
-
-func pwaFields(value string) []string {
- fields := []string{}
- start := -1
- for i, r := range value {
- if isPWASpace(r) {
- if start >= 0 {
- fields = append(fields, value[start:i])
- start = -1
- }
- continue
- }
- if start < 0 {
- start = i
- }
- }
- if start >= 0 {
- fields = append(fields, value[start:])
- }
- return fields
-}
-
-func trimPWAHyphens(value string) string {
- for len(value) > 0 && value[0] == '-' {
- value = value[1:]
- }
- for len(value) > 0 && value[len(value)-1] == '-' {
- value = value[:len(value)-1]
- }
- return value
-}
-
-func isPWAASCIILetter(r rune) bool {
- return r >= 'a' && r <= 'z'
-}
-
-func isPWAASCIIDigit(r rune) bool {
- return r >= '0' && r <= '9'
-}
-
-func isPWASpace(r rune) bool {
- return unicode.IsSpace(r)
-}
diff --git a/cmd/build/cmd_pwa_test.go b/cmd/build/cmd_pwa_test.go
deleted file mode 100644
index 5307b43..0000000
--- a/cmd/build/cmd_pwa_test.go
+++ /dev/null
@@ -1,186 +0,0 @@
-package buildcmd
-
-import (
- "context"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "dappco.re/go/build/internal/ax"
-)
-
-func TestPwa_FindManifestURLGood(t *testing.T) {
- t.Run("accepts a standard manifest link", func(t *testing.T) {
- htmlContent := `
`
-
- got := requireBuildCmdString(t, findManifestURL(htmlContent, "https://example.test/app/"))
- if !stdlibAssertEqual("https://example.test/manifest.json", got) {
- t.Fatalf("want %v, got %v", "https://example.test/manifest.json", got)
- }
-
- })
-
- t.Run("accepts case-insensitive tokenised rel values", func(t *testing.T) {
- htmlContent := ``
-
- got := requireBuildCmdString(t, findManifestURL(htmlContent, "https://example.test/app/"))
- if !stdlibAssertEqual("https://example.test/app/manifest.json", got) {
- t.Fatalf("want %v, got %v", "https://example.test/app/manifest.json", got)
- }
-
- })
-}
-
-func TestPwa_FindManifestURLBad(t *testing.T) {
- t.Run("returns an error when no manifest link exists", func(t *testing.T) {
- htmlContent := ``
-
- result := findManifestURL(htmlContent, "https://example.test/app/")
- message := requireBuildCmdError(t, result)
- got, _ := result.Value.(string)
- if !stdlibAssertEmpty(got) {
- t.Fatalf("expected empty, got %v", got)
- }
- if !stdlibAssertContains(message, "pwa.findManifestURL") {
- t.Fatalf("expected %v to contain %v", message, "pwa.findManifestURL")
- }
-
- })
-}
-
-func TestPwa_ExtractHTMLMetadataAndAssetsGood(t *testing.T) {
- htmlContent := `
-
-
-
- Example App
-
-
-
-
-
-
-
-
-
-`
-
- extracted := requireBuildCmdPWAExtraction(t, extractHTMLMetadataAndAssets(htmlContent, "https://example.test/app/"))
- metadata := extracted.Metadata
- assets := extracted.Assets
- if !stdlibAssertEqual("Example App", metadata.DisplayName) {
- t.Fatalf("want %v, got %v", "Example App", metadata.DisplayName)
- }
- if !stdlibAssertEqual("Example description", metadata.Description) {
- t.Fatalf("want %v, got %v", "Example description", metadata.Description)
- }
- if !stdlibAssertEqual("https://example.test/manifest.json", metadata.ManifestURL) {
- t.Fatalf("want %v, got %v", "https://example.test/manifest.json", metadata.ManifestURL)
- }
- if !stdlibAssertEqual([]string{"https://example.test/assets/icon.png"}, metadata.Icons) {
- t.Fatalf("want %v, got %v", []string{"https://example.test/assets/icon.png"}, metadata.Icons)
- }
- if !stdlibAssertElementsMatch([]string{"https://example.test/manifest.json", "https://example.test/assets/app.css", "https://example.test/assets/icon.png", "https://example.test/assets/app.js", "https://example.test/assets/logo.png", "https://example.test/assets/logo@2x.png"}, assets) {
- t.Fatalf("expected elements %v, got %v", []string{"https://example.test/manifest.json", "https://example.test/assets/app.css", "https://example.test/assets/icon.png", "https://example.test/assets/app.js", "https://example.test/assets/logo.png", "https://example.test/assets/logo@2x.png"}, assets)
- }
-
-}
-
-func TestPwa_DownloadPWA_DownloadsHTMLAndManifestAssetsGood(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.URL.Path {
- case "/app":
- _, _ = w.Write([]byte(`
-
-
- Example App
-
-
-
-
-
-
-
-
-`))
- case "/manifest.json":
- w.Header().Set("Content-Type", "application/manifest+json")
- _, _ = w.Write([]byte(`{
- "name": "Manifest App",
- "description": "Manifest description",
- "start_url": "/launch.html",
- "icons": [
- {"src": "/assets/icon-192.png"}
- ]
-}`))
- case "/assets/app.css":
- _, _ = w.Write([]byte("body { color: red; }"))
- case "/assets/app.js":
- _, _ = w.Write([]byte("console.log('app');"))
- case "/assets/logo.png":
- _, _ = w.Write([]byte("logo"))
- case "/assets/icon-192.png":
- _, _ = w.Write([]byte("icon"))
- case "/launch.html":
- _, _ = w.Write([]byte("launch"))
- default:
- http.NotFound(w, r)
- }
- }))
- defer server.Close()
-
- destDir := t.TempDir()
- requireBuildCmdOK(t, downloadPWA(context.Background(), server.URL+"/app", destDir))
-
- indexBody := requireBuildCmdBytes(t, ax.ReadFile(ax.Join(destDir, "index.html")))
- if !stdlibAssertContains(string(indexBody), "Example App") {
- t.Fatalf("expected %v to contain %v", string(indexBody), "Example App")
- }
-
- manifestBody := requireBuildCmdBytes(t, ax.ReadFile(ax.Join(destDir, "manifest.json")))
- if !stdlibAssertContains(string(manifestBody), `"name": "Manifest App"`) {
- t.Fatalf("expected %v to contain %v", string(manifestBody), `"name": "Manifest App"`)
- }
-
- for _, relPath := range []string{
- "assets/app.css",
- "assets/app.js",
- "assets/logo.png",
- "assets/icon-192.png",
- "launch.html",
- } {
- if !(ax.IsFile(ax.Join(destDir, relPath))) {
- t.Fatal(relPath)
- }
-
- }
-}
-
-func TestPwa_ResolvePWAAppConfig_UsesLocalMetadataGood(t *testing.T) {
- projectDir := t.TempDir()
- requireBuildCmdOK(t, ax.WriteString(ax.Join(projectDir, "index.html"), `
-
-
- Fallback Title
-
-
-
-`, 0o644))
- requireBuildCmdOK(t, ax.WriteString(ax.Join(projectDir, "manifest.json"), `{
- "name": "Manifest App",
- "description": "Manifest description",
- "icons": [{"src": "/icon.png"}]
-}`, 0o644))
-
- cfg := resolvePWAAppConfig(projectDir)
- if !stdlibAssertEqual("manifest-app", cfg.ModuleName) {
- t.Fatalf("want %v, got %v", "manifest-app", cfg.ModuleName)
- }
- if !stdlibAssertEqual("Manifest App", cfg.DisplayName) {
- t.Fatalf("want %v, got %v", "Manifest App", cfg.DisplayName)
- }
- if !stdlibAssertEqual("Manifest description", cfg.Description) {
- t.Fatalf("want %v, got %v", "Manifest description", cfg.Description)
- }
-
-}
diff --git a/cmd/build/cmd_release.go b/cmd/build/cmd_release.go
deleted file mode 100644
index ebfce9d..0000000
--- a/cmd/build/cmd_release.go
+++ /dev/null
@@ -1,208 +0,0 @@
-// cmd_release.go implements the release command: build + archive + publish in one step.
-
-package buildcmd
-
-import (
- "context"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/internal/cmdutil"
- "dappco.re/go/build/pkg/build"
- "dappco.re/go/build/pkg/release"
-)
-
-var (
- getReleaseWorkingDir = ax.Getwd
- releaseConfigExistsFn = release.ConfigExists
- loadReleaseConfigFn = release.LoadConfig
- runFullReleaseFn = release.Run
- runSDKReleaseFn = release.RunSDK
-)
-
-// AddReleaseCommand adds the release subcommand to the build command.
-//
-// buildcmd.AddReleaseCommand(buildCmd)
-func AddReleaseCommand(c *core.Core) {
- registerReleaseCommand(c, "build/release")
- registerReleaseCommand(c, "release")
-}
-
-func registerReleaseCommand(c *core.Core, path string) {
- c.Command(path, core.Command{
- Description: "cmd.build.release.long",
- Action: func(opts core.Options) core.Result {
- return runRelease(
- cmdutil.ContextOrBackground(),
- resolveReleaseDryRun(
- cmdutil.OptionBool(opts, "dry-run"),
- cmdutil.OptionBool(opts, "publish"),
- cmdutil.OptionBool(opts, "we-are-go-for-launch"),
- ),
- cmdutil.OptionBool(opts, "ci"),
- cmdutil.OptionString(opts, "target"),
- cmdutil.OptionString(opts, "version", "tag"),
- cmdutil.OptionBool(opts, "draft"),
- cmdutil.OptionBool(opts, "prerelease"),
- cmdutil.OptionString(opts, "archive-format"),
- cmdutil.OptionBool(opts, "apple-testflight", "apple_testflight", "testflight"),
- )
- },
- })
-}
-
-// runRelease executes the full release workflow: build + archive + checksum + publish.
-//
-// runRelease(ctx, true, false, "sdk", "v1.2.3", true, false, "xz") // dry run with an SDK-only target
-func runRelease(ctx context.Context, dryRun bool, ciMode bool, target, version string, draft, prerelease bool, archiveFormat string, appleTestFlightFlag ...bool) (result core.Result) {
- if ciMode {
- defer func() {
- emitCIErrorAnnotation(result)
- }()
- }
-
- // Get current directory
- projectDirResult := getReleaseWorkingDir()
- if !projectDirResult.OK {
- return core.Fail(core.E("release", "get working directory", core.NewError(projectDirResult.Error())))
- }
- projectDir := projectDirResult.Value.(string)
-
- target = core.Lower(core.Trim(target))
- if releaseAppleTestFlightRequested(target, appleTestFlightFlag...) {
- return runAppleBuildInDir(ctx, projectDir, appleCLIOptions{
- Version: version,
- TestFlight: true,
- TestFlightChanged: true,
- })
- }
- if target == "" {
- target = "release"
- }
-
- // Check for release config
- if !releaseConfigExistsFn(projectDir) {
- cli.Print("%s %s\n",
- buildErrorStyle.Render("error:"),
- "release config not found",
- )
- cli.Print(" %s\n", buildDimStyle.Render("Run core ci/init to create .core/release.yaml"))
- return core.Fail(core.E("release", "config not found", nil))
- }
-
- // Load configuration
- cfgResult := loadReleaseConfigFn(projectDir)
- if !cfgResult.OK {
- return core.Fail(core.E("release", "load config", core.NewError(cfgResult.Error())))
- }
- cfg := cfgResult.Value.(*release.Config)
-
- // Apply CLI overrides
- if version != "" {
- if !release.ValidateVersion(version) {
- return core.Fail(core.E("release", "invalid release version override", nil))
- }
- cfg.SetVersion(version)
- }
- archiveFormatOverride := applyReleaseArchiveFormatOverride(cfg, archiveFormat)
- if !archiveFormatOverride.OK {
- return archiveFormatOverride
- }
-
- // Apply draft/prerelease overrides to all publishers
- if target == "release" && (draft || prerelease) {
- for i := range cfg.Publishers {
- if draft {
- cfg.Publishers[i].Draft = true
- }
- if prerelease {
- cfg.Publishers[i].Prerelease = true
- }
- }
- }
-
- // Print header
- cli.Print("%s %s\n", buildHeaderStyle.Render("Release"), releaseTargetLabel(target))
- if dryRun {
- cli.Print(" %s\n", buildDimStyle.Render("Dry run: no publishers will be changed"))
- }
- cli.Blank()
-
- switch target {
- case "release":
- relResult := runFullReleaseFn(ctx, cfg, dryRun)
- if !relResult.OK {
- return relResult
- }
- rel := relResult.Value.(*release.Release)
-
- // Print summary
- cli.Blank()
- cli.Print("%s %s\n", buildSuccessStyle.Render("Done"), "Release completed")
- cli.Print(" %s %s\n", "version:", buildTargetStyle.Render(rel.Version))
- cli.Print(" %s %d\n", "artifacts", len(rel.Artifacts))
-
- if !dryRun {
- for _, pub := range cfg.Publishers {
- cli.Print(" %s %s\n", "published", buildTargetStyle.Render(pub.Type))
- }
- }
-
- return core.Ok(nil)
- case "sdk":
- sdkResult := runSDKReleaseFn(ctx, cfg, dryRun)
- if !sdkResult.OK {
- return sdkResult
- }
- sdkRelease := sdkResult.Value.(*release.SDKRelease)
-
- cli.Blank()
- cli.Print("%s %s\n", buildSuccessStyle.Render("Done"), "SDK release completed")
- cli.Print(" %s %s\n", "version:", buildTargetStyle.Render(sdkRelease.Version))
- cli.Print(" %s %s\n", "output", buildTargetStyle.Render(sdkRelease.Output))
- cli.Print(" %s %s\n", "languages", buildTargetStyle.Render(core.Join(", ", sdkRelease.Languages...)))
- return core.Ok(nil)
- default:
- return core.Fail(core.E("release", "unsupported release target: "+target, nil))
- }
-}
-
-// applyReleaseArchiveFormatOverride applies the archive-format CLI override to the release config.
-//
-// applyReleaseArchiveFormatOverride(cfg, "xz") // cfg.Build.ArchiveFormat = "xz"
-func applyReleaseArchiveFormatOverride(cfg *release.Config, archiveFormat string) core.Result {
- if cfg == nil || archiveFormat == "" {
- return core.Ok(nil)
- }
-
- formatValue := resolveArchiveFormat("", archiveFormat)
- if !formatValue.OK {
- return formatValue
- }
-
- cfg.Build.ArchiveFormat = string(formatValue.Value.(build.ArchiveFormat))
- return core.Ok(nil)
-}
-
-func releaseAppleTestFlightRequested(target string, appleTestFlightFlag ...bool) bool {
- if len(appleTestFlightFlag) > 0 && appleTestFlightFlag[0] {
- return true
- }
-
- return target == "apple-testflight" || target == "testflight"
-}
-
-func resolveReleaseDryRun(dryRun, publish, weAreGoForLaunch bool) bool {
- if publish || weAreGoForLaunch {
- return false
- }
- return dryRun
-}
-
-func releaseTargetLabel(target string) string {
- if target == "sdk" {
- return "Generating SDK release"
- }
- return "Building and publishing"
-}
diff --git a/cmd/build/cmd_release_example_test.go b/cmd/build/cmd_release_example_test.go
deleted file mode 100644
index d9c040c..0000000
--- a/cmd/build/cmd_release_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package buildcmd
-
-import core "dappco.re/go"
-
-// ExampleAddReleaseCommand references AddReleaseCommand on this package API surface.
-func ExampleAddReleaseCommand() {
- _ = AddReleaseCommand
- core.Println("AddReleaseCommand")
- // Output: AddReleaseCommand
-}
diff --git a/cmd/build/cmd_release_test.go b/cmd/build/cmd_release_test.go
deleted file mode 100644
index e485d42..0000000
--- a/cmd/build/cmd_release_test.go
+++ /dev/null
@@ -1,278 +0,0 @@
-package buildcmd
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/pkg/build"
- "dappco.re/go/build/pkg/release"
-)
-
-func TestBuildCmd_applyReleaseArchiveFormatOverride_Good(t *testing.T) {
- cfg := release.DefaultConfig()
-
- requireBuildCmdOK(t, applyReleaseArchiveFormatOverride(cfg, "xz"))
- if !stdlibAssertEqual("xz", cfg.Build.ArchiveFormat) {
- t.Fatalf("want %v, got %v", "xz", cfg.Build.ArchiveFormat)
- }
-
-}
-
-func TestBuildCmd_applyReleaseArchiveFormatOverride_Bad(t *testing.T) {
- cfg := release.DefaultConfig()
-
- requireBuildCmdError(t, applyReleaseArchiveFormatOverride(cfg, "bogus"))
- if !stdlibAssertEqual("", cfg.Build.ArchiveFormat) {
- t.Fatalf("want %v, got %v", "", cfg.Build.ArchiveFormat)
- }
-
-}
-
-func TestBuildCmd_AddReleaseCommand_RegistersTopLevelAlias_Good(t *testing.T) {
- c := core.New()
-
- AddReleaseCommand(c)
- if !(c.Command("build/release").OK) {
- t.Fatal("expected true")
- }
- if !(c.Command("release").OK) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestBuildCmd_resolveReleaseDryRun_Good(t *testing.T) {
- if resolveReleaseDryRun(false, false, false) {
- t.Fatal("expected false")
- }
- if !(resolveReleaseDryRun(true, false, false)) {
- t.Fatal("expected true")
- }
- if resolveReleaseDryRun(false, true, false) {
- t.Fatal("expected false")
- }
- if resolveReleaseDryRun(true, true, false) {
- t.Fatal("expected false")
- }
- if resolveReleaseDryRun(false, false, true) {
- t.Fatal("expected false")
- }
- if resolveReleaseDryRun(true, false, true) {
- t.Fatal("expected false")
- }
-
-}
-
-func TestBuildCmd_runRelease_TargetSDK_Good(t *testing.T) {
- projectDir := t.TempDir()
- originalGetwd := getReleaseWorkingDir
- t.Cleanup(func() {
- getReleaseWorkingDir = originalGetwd
- })
- getReleaseWorkingDir = func() core.Result { return core.Ok(projectDir) }
-
- originalConfigExists := releaseConfigExistsFn
- originalLoadConfig := loadReleaseConfigFn
- originalRunSDK := runSDKReleaseFn
- t.Cleanup(func() {
- releaseConfigExistsFn = originalConfigExists
- loadReleaseConfigFn = originalLoadConfig
- runSDKReleaseFn = originalRunSDK
- })
-
- releaseConfigExistsFn = func(dir string) bool {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
-
- return true
- }
- loadReleaseConfigFn = func(dir string) core.Result {
- cfg := release.DefaultConfig()
- cfg.SetProjectDir(dir)
- cfg.SDK = &release.SDKConfig{
- Languages: []string{"typescript", "go"},
- Output: "sdk",
- }
- return core.Ok(cfg)
- }
-
- called := false
- runSDKReleaseFn = func(ctx context.Context, cfg *release.Config, dryRun bool) core.Result {
- called = true
- if !(dryRun) {
- t.Fatal("expected true")
- }
- if stdlibAssertNil(cfg.SDK) {
- t.Fatal("expected non-nil")
- }
-
- return core.Ok(&release.SDKRelease{
- Version: "v1.2.3",
- Output: "sdk",
- Languages: []string{"typescript", "go"},
- })
- }
-
- requireBuildCmdOK(t, runRelease(context.Background(), true, false, "sdk", "v1.2.3", false, false, ""))
- if !(called) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestBuildCmd_runRelease_AppleTestFlight_Good(t *testing.T) {
- projectDir := t.TempDir()
- requireBuildCmdOK(t, ax.MkdirAll(ax.Join(projectDir, ".core"), 0o755))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(projectDir, ".core", "build.yaml"), []byte(`
-project:
- name: Core
- binary: Core
-apple:
- bundle_id: ai.lthn.core
-`), 0o644))
-
- originalGetwd := getReleaseWorkingDir
- originalConfigExists := releaseConfigExistsFn
- originalBuildApple := buildAppleFn
- t.Cleanup(func() {
- getReleaseWorkingDir = originalGetwd
- releaseConfigExistsFn = originalConfigExists
- buildAppleFn = originalBuildApple
- })
-
- getReleaseWorkingDir = func() core.Result { return core.Ok(projectDir) }
- releaseConfigExistsFn = func(dir string) bool {
- t.Fatalf("release config should not be required for apple-testflight target: %s", dir)
- return false
- }
-
- called := false
- buildAppleFn = func(ctx context.Context, cfg *build.Config, options build.AppleOptions, buildNumber string) core.Result {
- called = true
- if !stdlibAssertEqual(projectDir, cfg.ProjectDir) {
- t.Fatalf("want %v, got %v", projectDir, cfg.ProjectDir)
- }
- if !stdlibAssertEqual("v1.2.3", cfg.Version) {
- t.Fatalf("want %v, got %v", "v1.2.3", cfg.Version)
- }
- if !stdlibAssertEqual("ai.lthn.core", options.BundleID) {
- t.Fatalf("want %v, got %v", "ai.lthn.core", options.BundleID)
- }
- if !options.TestFlight {
- t.Fatal("expected TestFlight")
- }
- if !stdlibAssertEqual("1", buildNumber) {
- t.Fatalf("want %v, got %v", "1", buildNumber)
- }
- return core.Ok(&build.AppleBuildResult{
- BundlePath: ax.Join(cfg.OutputDir, "Core.app"),
- Version: "1.2.3",
- BuildNumber: buildNumber,
- })
- }
-
- requireBuildCmdOK(t, runRelease(context.Background(), false, false, "apple-testflight", "v1.2.3", false, false, ""))
- if !called {
- t.Fatal("expected buildAppleFn to be called")
- }
-}
-
-func TestBuildCmd_releaseAppleTestFlightRequested_Good(t *testing.T) {
- if !releaseAppleTestFlightRequested("apple-testflight") {
- t.Fatal("expected apple-testflight target to request TestFlight")
- }
- if !releaseAppleTestFlightRequested("testflight") {
- t.Fatal("expected testflight target to request TestFlight")
- }
- if !releaseAppleTestFlightRequested("release", true) {
- t.Fatal("expected explicit flag to request TestFlight")
- }
- if releaseAppleTestFlightRequested("release") {
- t.Fatal("expected release target without flag to skip TestFlight")
- }
-}
-
-func TestBuildCmd_runRelease_RejectsUnsafeVersion_Bad(t *testing.T) {
- projectDir := t.TempDir()
- originalGetwd := getReleaseWorkingDir
- originalConfigExists := releaseConfigExistsFn
- t.Cleanup(func() {
- getReleaseWorkingDir = originalGetwd
- releaseConfigExistsFn = originalConfigExists
- })
-
- getReleaseWorkingDir = func() core.Result { return core.Ok(projectDir) }
- releaseConfigExistsFn = func(dir string) bool { return true }
-
- message := requireBuildCmdError(t, runRelease(context.Background(), true, false, "release", "v1.2.3 --bad", false, false, ""))
- if !stdlibAssertContains(message, "invalid release version override") {
- t.Fatalf("expected %v to contain %v", message, "invalid release version override")
- }
-
-}
-
-func TestBuildCmd_runRelease_CIModeEmitsGitHubAnnotationOnError_Bad(t *testing.T) {
- projectDir := t.TempDir()
- originalGetwd := getReleaseWorkingDir
- originalConfigExists := releaseConfigExistsFn
- t.Cleanup(func() {
- getReleaseWorkingDir = originalGetwd
- releaseConfigExistsFn = originalConfigExists
- cli.SetStdout(nil)
- cli.SetStderr(nil)
- })
-
- getReleaseWorkingDir = func() core.Result { return core.Ok(projectDir) }
- releaseConfigExistsFn = func(dir string) bool {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
-
- return false
- }
-
- stdout := core.NewBuffer()
- cli.SetStdout(stdout)
- cli.SetStderr(stdout)
-
- result := runRelease(context.Background(), false, true, "release", "", false, false, "")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(stdout.String(), emitCIAnnotationForTest(result)) {
- t.Fatalf("expected %v to contain %v", stdout.String(), emitCIAnnotationForTest(result))
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCmdRelease_AddReleaseCommand_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- AddReleaseCommand(core.New())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCmdRelease_AddReleaseCommand_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- AddReleaseCommand(core.New())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCmdRelease_AddReleaseCommand_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- AddReleaseCommand(core.New())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/cmd/build/cmd_sdk.go b/cmd/build/cmd_sdk.go
deleted file mode 100644
index b96520c..0000000
--- a/cmd/build/cmd_sdk.go
+++ /dev/null
@@ -1,116 +0,0 @@
-// cmd_sdk.go implements SDK generation from OpenAPI specifications.
-//
-// Generates typed API clients for TypeScript, Python, Go, and PHP
-// from OpenAPI/Swagger specifications.
-
-package buildcmd
-
-import (
- "context"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/internal/sdkcfg"
- "dappco.re/go/build/pkg/sdk"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// runBuildSDK handles the `core build sdk` command.
-func runBuildSDK(ctx context.Context, specPath, lang, version string, dryRun bool, skipUnavailable bool) core.Result {
- projectDirResult := ax.Getwd()
- if !projectDirResult.OK {
- return core.Fail(core.E("build.SDK", "failed to get working directory", core.NewError(projectDirResult.Error())))
- }
-
- return runBuildSDKInDir(ctx, projectDirResult.Value.(string), specPath, lang, version, dryRun, skipUnavailable)
-}
-
-func runBuildSDKInDir(ctx context.Context, projectDir, specPath, lang, version string, dryRun bool, skipUnavailable bool) core.Result {
- configResult := sdkcfg.LoadProjectConfig(storage.Local, projectDir)
- if !configResult.OK {
- return core.Fail(core.E("build.SDK", "failed to load sdk config", core.NewError(configResult.Error())))
- }
- config := configResult.Value.(*sdk.Config)
- if specPath != "" {
- config.Spec = specPath
- }
- if skipUnavailable {
- config.SkipUnavailable = true
- }
-
- s := sdk.New(projectDir, config)
- if version != "" {
- s.SetVersion(version)
- }
- resolvedConfig := s.Config()
-
- cli.Print("%s %s\n", buildHeaderStyle.Render("SDK"), "Generating SDK")
- if dryRun {
- cli.Print(" %s\n", buildDimStyle.Render("dry run mode"))
- }
- cli.Blank()
-
- // Validate the spec before generating anything.
- detectedSpecResult := s.ValidateSpec(ctx)
- if !detectedSpecResult.OK {
- cli.Print("%s %v\n", buildErrorStyle.Render("error"), detectedSpecResult.Error())
- return detectedSpecResult
- }
- detectedSpec := detectedSpecResult.Value.(string)
- cli.Print(" %s %s\n", "spec", buildTargetStyle.Render(detectedSpec))
-
- if dryRun {
- if lang != "" {
- cli.Print(" %s %s\n", "language", buildTargetStyle.Render(lang))
- } else {
- cli.Print(" %s %s\n", "languages", buildTargetStyle.Render(core.Join(", ", resolvedConfig.Languages...)))
- }
- cli.Blank()
- cli.Print("%s %s\n", buildSuccessStyle.Render("OK"), "Would generate SDK")
- return core.Ok(nil)
- }
-
- if lang != "" {
- // Generate single language
- resultResult := s.GenerateLanguageWithStatus(ctx, lang)
- if !resultResult.OK {
- cli.Print("%s %v\n", buildErrorStyle.Render("error"), resultResult.Error())
- return resultResult
- }
- result := resultResult.Value.(sdk.LanguageResult)
- if result.Skipped {
- cli.Print(" %s %s\n", "Skipped:", buildTargetStyle.Render(result.Language))
- } else {
- cli.Print(" %s %s\n", "generated", buildTargetStyle.Render(result.Language))
- }
- } else {
- // Generate all
- resultsResult := s.GenerateWithStatus(ctx)
- if !resultsResult.OK {
- cli.Print("%s %v\n", buildErrorStyle.Render("error"), resultsResult.Error())
- return resultsResult
- }
- results := resultsResult.Value.([]sdk.LanguageResult)
- generated := make([]string, 0, len(results))
- skipped := make([]string, 0)
- for _, result := range results {
- if result.Generated {
- generated = append(generated, result.Language)
- }
- if result.Skipped {
- skipped = append(skipped, result.Language)
- }
- }
- if len(generated) > 0 {
- cli.Print(" %s %s\n", "generated", buildTargetStyle.Render(core.Join(", ", generated...)))
- }
- if len(skipped) > 0 {
- cli.Print(" %s %s\n", "Skipped:", buildTargetStyle.Render(core.Join(", ", skipped...)))
- }
- }
-
- cli.Blank()
- cli.Print("%s %s\n", buildSuccessStyle.Render("Success"), "SDK generation complete")
- return core.Ok(nil)
-}
diff --git a/cmd/build/cmd_sdk_test.go b/cmd/build/cmd_sdk_test.go
deleted file mode 100644
index dd6b08b..0000000
--- a/cmd/build/cmd_sdk_test.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package buildcmd
-
-import (
- "context"
- "testing"
-
- "dappco.re/go/build/internal/ax"
-)
-
-const validBuildOpenAPISpec = `openapi: "3.0.0"
-info:
- title: Test API
- version: "1.0.0"
-paths:
- /health:
- get:
- operationId: getHealth
- responses:
- "200":
- description: OK
-`
-
-func TestRunBuildSDKInDir_ValidSpecDryRunGood(t *testing.T) {
- tmpDir := t.TempDir()
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(tmpDir, "openapi.yaml"), []byte(validBuildOpenAPISpec), 0o644))
-
- requireBuildCmdOK(t, runBuildSDKInDir(context.Background(), tmpDir, "", "go", "", true, false))
-
-}
-
-func TestRunBuildSDKInDir_UsesBuildSDKConfigGood(t *testing.T) {
- tmpDir := t.TempDir()
- specPath := ax.Join(tmpDir, "docs", "openapi.yaml")
- requireBuildCmdOK(t, ax.MkdirAll(ax.Dir(specPath), 0o755))
- requireBuildCmdOK(t, ax.WriteFile(specPath, []byte(validBuildOpenAPISpec), 0o644))
- requireBuildCmdOK(t, ax.MkdirAll(ax.Join(tmpDir, ".core"), 0o755))
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(tmpDir, ".core", "build.yaml"), []byte(`version: 1
-sdk:
- spec: docs/openapi.yaml
- languages:
- - go
-`), 0o644))
-
- requireBuildCmdOK(t, runBuildSDKInDir(context.Background(), tmpDir, "", "", "", true, false))
-
-}
-
-func TestRunBuildSDKInDir_InvalidDocumentBad(t *testing.T) {
- tmpDir := t.TempDir()
- requireBuildCmdOK(t, ax.WriteFile(ax.Join(tmpDir, "openapi.yaml"), []byte(`openapi: "3.0.0"
-info:
- title: Test API
-paths: {}
-`), 0o644))
-
- message := requireBuildCmdError(t, runBuildSDKInDir(context.Background(), tmpDir, "", "", "", true, false))
- if !stdlibAssertContains(message, "invalid OpenAPI spec") {
- t.Fatalf("expected %v to contain %v", message, "invalid OpenAPI spec")
- }
-
-}
diff --git a/cmd/build/cmd_service.go b/cmd/build/cmd_service.go
deleted file mode 100644
index e1419de..0000000
--- a/cmd/build/cmd_service.go
+++ /dev/null
@@ -1,208 +0,0 @@
-// cmd_service.go registers native OS service management for the build daemon.
-package buildcmd
-
-import (
- "context"
- "os/signal"
- "syscall"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cli"
- "dappco.re/go/build/internal/cmdutil"
- servicecommon "dappco.re/go/build/internal/servicecmd"
- buildservice "dappco.re/go/build/pkg/service"
-)
-
-var (
- serviceGetwd = ax.Getwd
- resolveBuildServiceCfg = buildservice.ResolveConfig
- exportBuildService = buildservice.Export
- runBuildServiceDaemon = buildservice.Run
- buildServiceManager = buildservice.NewManager()
-)
-
-type serviceRequest = servicecommon.Request
-
-// AddServiceCommands registers `core service` commands.
-func AddServiceCommands(c *core.Core) {
- c.Command("service", core.Command{
- Description: "cmd.service.short",
- Action: func(opts core.Options) core.Result {
- return core.Fail(core.E("service", "use a subcommand: install, start, stop, uninstall, export", nil))
- },
- })
-
- c.Command("service/install", core.Command{
- Description: "cmd.service.install.short",
- Action: func(opts core.Options) core.Result {
- return runServiceInstall(serviceRequestFromOptions(opts))
- },
- })
-
- c.Command("service/start", core.Command{
- Description: "cmd.service.start.short",
- Action: func(opts core.Options) core.Result {
- return runServiceStart(serviceRequestFromOptions(opts))
- },
- })
-
- c.Command("service/stop", core.Command{
- Description: "cmd.service.stop.short",
- Action: func(opts core.Options) core.Result {
- return runServiceStop(serviceRequestFromOptions(opts))
- },
- })
-
- c.Command("service/uninstall", core.Command{
- Description: "cmd.service.uninstall.short",
- Action: func(opts core.Options) core.Result {
- return runServiceUninstall(serviceRequestFromOptions(opts))
- },
- })
-
- c.Command("service/export", core.Command{
- Description: "cmd.service.export.short",
- Action: func(opts core.Options) core.Result {
- return runServiceExport(serviceRequestFromOptions(opts))
- },
- })
-
- c.Command("service/run", core.Command{
- Description: "cmd.service.run.short",
- Hidden: true,
- Action: func(opts core.Options) core.Result {
- return runServiceRun(cmdutil.ContextOrBackground(), serviceRequestFromOptions(opts))
- },
- })
-}
-
-func serviceRequestFromOptions(opts core.Options) serviceRequest {
- return servicecommon.FromOptions(opts)
-}
-
-func runServiceInstall(req serviceRequest) core.Result {
- cfgResult := loadServiceConfig(req)
- if !cfgResult.OK {
- return cfgResult
- }
- cfg := cfgResult.Value.(buildservice.Config)
-
- cli.Print("%s %s\n", buildHeaderStyle.Render("Service"), "Installing daemon service")
- cli.Print(" name %s\n", buildTargetStyle.Render(cfg.Name))
- cli.Print(" addr %s\n", buildTargetStyle.Render(cfg.APIAddr))
- cli.Print(" health %s\n", buildTargetStyle.Render(cfg.HealthAddr))
-
- installed := buildServiceManager.Install(cfg)
- if !installed.OK {
- return installed
- }
-
- cli.Print("%s %s\n", buildSuccessStyle.Render("Done"), "Service installed")
- return core.Ok(nil)
-}
-
-func runServiceStart(req serviceRequest) core.Result {
- cfgResult := loadServiceConfig(req)
- if !cfgResult.OK {
- return cfgResult
- }
- cfg := cfgResult.Value.(buildservice.Config)
-
- started := buildServiceManager.Start(cfg)
- if !started.OK {
- return started
- }
- cli.Print("%s %s\n", buildSuccessStyle.Render("Done"), "Service started")
- return core.Ok(nil)
-}
-
-func runServiceStop(req serviceRequest) core.Result {
- cfgResult := loadServiceConfig(req)
- if !cfgResult.OK {
- return cfgResult
- }
- cfg := cfgResult.Value.(buildservice.Config)
-
- stopped := buildServiceManager.Stop(cfg)
- if !stopped.OK {
- return stopped
- }
- cli.Print("%s %s\n", buildSuccessStyle.Render("Done"), "Service stopped")
- return core.Ok(nil)
-}
-
-func runServiceUninstall(req serviceRequest) core.Result {
- cfgResult := loadServiceConfig(req)
- if !cfgResult.OK {
- return cfgResult
- }
- cfg := cfgResult.Value.(buildservice.Config)
-
- uninstalled := buildServiceManager.Uninstall(cfg)
- if !uninstalled.OK {
- return uninstalled
- }
- cli.Print("%s %s\n", buildSuccessStyle.Render("Done"), "Service uninstalled")
- return core.Ok(nil)
-}
-
-func runServiceExport(req serviceRequest) core.Result {
- cfgResult := loadServiceConfig(req)
- if !cfgResult.OK {
- return cfgResult
- }
- cfg := cfgResult.Value.(buildservice.Config)
-
- exportedResult := exportBuildService(cfg, req.Format)
- if !exportedResult.OK {
- return exportedResult
- }
- exported := exportedResult.Value.(buildservice.ExportedConfig)
-
- if req.Output == "" {
- cli.Print("%s", exported.Content)
- return core.Ok(nil)
- }
-
- outputPath := req.Output
- if !core.PathIsAbs(outputPath) {
- outputPath = core.PathJoin(cfg.ProjectDir, outputPath)
- }
- created := ax.MkdirAll(core.PathDir(outputPath), 0o755)
- if !created.OK {
- return created
- }
- written := ax.WriteFile(outputPath, []byte(exported.Content), 0o644)
- if !written.OK {
- return written
- }
-
- cli.Print("%s %s\n", buildSuccessStyle.Render("Done"), outputPath)
- return core.Ok(nil)
-}
-
-func runServiceRun(ctx context.Context, req serviceRequest) core.Result {
- cfgResult := loadServiceConfig(req)
- if !cfgResult.OK {
- return cfgResult
- }
- cfg := cfgResult.Value.(buildservice.Config)
-
- signalContext, stop := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)
- defer stop()
-
- return runBuildServiceDaemon(signalContext, cfg)
-}
-
-func loadServiceConfig(req serviceRequest) core.Result {
- return servicecommon.LoadConfig(req, serviceGetwd, resolveBuildServiceCfg)
-}
-
-func applyServiceOverrides(cfg *buildservice.Config, req serviceRequest) core.Result {
- return servicecommon.ApplyOverrides(cfg, req)
-}
-
-func parseServiceCSV(value string) []string {
- return servicecommon.ParseCSV(value)
-}
diff --git a/cmd/build/cmd_service_example_test.go b/cmd/build/cmd_service_example_test.go
deleted file mode 100644
index e98a7f3..0000000
--- a/cmd/build/cmd_service_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package buildcmd
-
-import core "dappco.re/go"
-
-// ExampleAddServiceCommands references AddServiceCommands on this package API surface.
-func ExampleAddServiceCommands() {
- _ = AddServiceCommands
- core.Println("AddServiceCommands")
- // Output: AddServiceCommands
-}
diff --git a/cmd/build/cmd_service_test.go b/cmd/build/cmd_service_test.go
deleted file mode 100644
index de68b22..0000000
--- a/cmd/build/cmd_service_test.go
+++ /dev/null
@@ -1,222 +0,0 @@
-package buildcmd
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- buildservice "dappco.re/go/build/pkg/service"
-)
-
-type stubBuildServiceManager struct {
- install func(buildservice.Config) core.Result
- start func(buildservice.Config) core.Result
- stop func(buildservice.Config) core.Result
- remove func(buildservice.Config) core.Result
-}
-
-func (s stubBuildServiceManager) Install(cfg buildservice.Config) core.Result {
- if s.install != nil {
- return s.install(cfg)
- }
- return core.Ok(nil)
-}
-
-func (s stubBuildServiceManager) Start(cfg buildservice.Config) core.Result {
- if s.start != nil {
- return s.start(cfg)
- }
- return core.Ok(nil)
-}
-
-func (s stubBuildServiceManager) Stop(cfg buildservice.Config) core.Result {
- if s.stop != nil {
- return s.stop(cfg)
- }
- return core.Ok(nil)
-}
-
-func (s stubBuildServiceManager) Uninstall(cfg buildservice.Config) core.Result {
- if s.remove != nil {
- return s.remove(cfg)
- }
- return core.Ok(nil)
-}
-
-func restoreServiceCommandStubs(t *testing.T) {
- t.Helper()
-
- originalGetwd := serviceGetwd
- originalResolve := resolveBuildServiceCfg
- originalExport := exportBuildService
- originalRunDaemon := runBuildServiceDaemon
- originalManager := buildServiceManager
-
- t.Cleanup(func() {
- serviceGetwd = originalGetwd
- resolveBuildServiceCfg = originalResolve
- exportBuildService = originalExport
- runBuildServiceDaemon = originalRunDaemon
- buildServiceManager = originalManager
- })
-}
-
-func stubResolvedServiceConfig(t *testing.T, projectDir string) {
- t.Helper()
-
- serviceGetwd = func() core.Result { return core.Ok(projectDir) }
- resolveBuildServiceCfg = func(dir string) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
- return core.Ok(buildservice.Config{
- Name: "core-build",
- DisplayName: "Core Build",
- Description: "Core build daemon",
- ProjectDir: projectDir,
- APIAddr: "127.0.0.1:9101",
- HealthAddr: "127.0.0.1:9102",
- })
- }
-}
-
-func TestService_AddServiceCommands_RegistersSubcommandsGood(t *testing.T) {
- c := core.New()
-
- AddBuildCommands(c)
- for _, path := range []string{
- "service",
- "service/install",
- "service/start",
- "service/stop",
- "service/uninstall",
- "service/export",
- } {
- if !(c.Command(path).OK) {
- t.Fatalf("expected command to be registered: %s", path)
- }
- }
-
- command := c.Command("service/install").Value.(*core.Command)
- if !stdlibAssertEqual("cmd.service.install.short", command.Description) {
- t.Fatalf("want %v, got %v", "cmd.service.install.short", command.Description)
- }
-}
-
-func TestService_InstallGood(t *testing.T) {
- restoreServiceCommandStubs(t)
-
- projectDir := t.TempDir()
- stubResolvedServiceConfig(t, projectDir)
-
- called := false
- buildServiceManager = stubBuildServiceManager{
- install: func(cfg buildservice.Config) core.Result {
- called = true
- if !stdlibAssertEqual(projectDir, cfg.ProjectDir) {
- t.Fatalf("want %v, got %v", projectDir, cfg.ProjectDir)
- }
- if !stdlibAssertEqual("core-build", cfg.Name) {
- t.Fatalf("want %v, got %v", "core-build", cfg.Name)
- }
- return core.Ok(nil)
- },
- }
-
- requireBuildCmdOK(t, runServiceInstall(serviceRequest{}))
- if !called {
- t.Fatal("expected true")
- }
-}
-
-func TestService_InstallBad(t *testing.T) {
- restoreServiceCommandStubs(t)
-
- projectDir := t.TempDir()
- stubResolvedServiceConfig(t, projectDir)
-
- buildServiceManager = stubBuildServiceManager{
- install: func(buildservice.Config) core.Result {
- return core.Fail(core.NewError("native service unavailable"))
- },
- }
-
- message := requireBuildCmdError(t, runServiceInstall(serviceRequest{}))
- if !stdlibAssertContains(message, "native service unavailable") {
- t.Fatalf("expected %v to contain %v", message, "native service unavailable")
- }
-}
-
-func TestService_InstallUgly(t *testing.T) {
- restoreServiceCommandStubs(t)
-
- projectDir := t.TempDir()
- stubResolvedServiceConfig(t, projectDir)
-
- actions := make([]string, 0, 1)
- buildServiceManager = stubBuildServiceManager{
- install: func(buildservice.Config) core.Result {
- actions = append(actions, "install")
- return core.Fail(core.NewError("install rejected"))
- },
- }
-
- message := requireBuildCmdError(t, runServiceInstall(serviceRequest{}))
- if !stdlibAssertContains(message, "install rejected") {
- t.Fatalf("expected %v to contain %v", message, "install rejected")
- }
- if !stdlibAssertEqual([]string{"install"}, actions) {
- t.Fatalf("want %v, got %v", []string{"install"}, actions)
- }
-}
-
-func TestService_Run_InvokesDaemonGood(t *testing.T) {
- restoreServiceCommandStubs(t)
-
- projectDir := t.TempDir()
- stubResolvedServiceConfig(t, projectDir)
-
- daemonConfigs := make(chan buildservice.Config, 1)
- runBuildServiceDaemon = func(ctx context.Context, cfg buildservice.Config) core.Result {
- daemonConfigs <- cfg
- return core.Ok(nil)
- }
-
- requireBuildCmdOK(t, runServiceRun(context.Background(), serviceRequest{}))
- select {
- case cfg := <-daemonConfigs:
- if !stdlibAssertEqual(projectDir, cfg.ProjectDir) {
- t.Fatalf("want %v, got %v", projectDir, cfg.ProjectDir)
- }
- default:
- t.Fatal("expected daemon to be called")
- }
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCmdService_AddServiceCommands_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- AddServiceCommands(core.New())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCmdService_AddServiceCommands_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- AddServiceCommands(core.New())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCmdService_AddServiceCommands_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- AddServiceCommands(core.New())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/cmd/build/cmd_workflow.go b/cmd/build/cmd_workflow.go
deleted file mode 100644
index eb94691..0000000
--- a/cmd/build/cmd_workflow.go
+++ /dev/null
@@ -1,176 +0,0 @@
-// cmd_workflow.go implements the release workflow generation command.
-
-package buildcmd
-
-import (
- "context"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/cmdutil"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// releaseWorkflowRequestInputs keeps the workflow alias inputs grouped by the
-// public request fields they represent, rather than by call-site position.
-type releaseWorkflowRequestInputs struct {
- pathInput string
- workflowPathInput string
- workflowPathSnakeInput string
- workflowPathHyphenInput string
- outputPathInput string
- outputPathHyphenInput string
- outputPathSnakeInput string
- legacyOutputInput string
- workflowOutputPathInput string
- workflowOutputSnakeInput string
- workflowOutputHyphenInput string
- workflowOutputPathHyphenInput string
- workflowOutputPathSnakeInput string
-}
-
-// resolveReleaseWorkflowTargetPath merges the workflow path aliases and the
-// workflow output aliases into one final target path.
-//
-// inputs := releaseWorkflowRequestInputs{pathInput: "ci/release.yml", outputPathInput: "ci/release.yml"}
-// path, err := inputs.resolveReleaseWorkflowTargetPath("/tmp/project", storage.Local)
-func (inputs releaseWorkflowRequestInputs) resolveReleaseWorkflowTargetPath(projectDir string, medium storage.Medium) core.Result {
- resolvedWorkflowPath := resolveReleaseWorkflowInputPathAliases(
- projectDir,
- inputs.pathInput,
- inputs.workflowPathInput,
- inputs.workflowPathSnakeInput,
- inputs.workflowPathHyphenInput,
- )
- if !resolvedWorkflowPath.OK {
- return resolvedWorkflowPath
- }
-
- resolvedWorkflowOutputPath := resolveReleaseWorkflowOutputPathAliases(
- projectDir,
- inputs.outputPathInput,
- inputs.outputPathHyphenInput,
- inputs.outputPathSnakeInput,
- inputs.legacyOutputInput,
- inputs.workflowOutputPathInput,
- inputs.workflowOutputSnakeInput,
- inputs.workflowOutputHyphenInput,
- inputs.workflowOutputPathSnakeInput,
- inputs.workflowOutputPathHyphenInput,
- )
- if !resolvedWorkflowOutputPath.OK {
- return resolvedWorkflowOutputPath
- }
-
- return build.ResolveReleaseWorkflowInputPathWithMedium(medium, projectDir, resolvedWorkflowPath.Value.(string), resolvedWorkflowOutputPath.Value.(string))
-}
-
-// AddWorkflowCommand registers the build/workflow subcommand.
-func AddWorkflowCommand(c *core.Core) {
- c.Command("build/workflow", core.Command{
- Description: "cmd.build.workflow.long",
- Action: func(opts core.Options) core.Result {
- return runReleaseWorkflow(cmdutil.ContextOrBackground(), releaseWorkflowRequestInputs{
- pathInput: cmdutil.OptionString(opts, buildPathOptionKey),
- workflowPathInput: cmdutil.OptionString(opts, "workflowPath"),
- workflowPathSnakeInput: cmdutil.OptionString(opts, "workflow_path"),
- workflowPathHyphenInput: cmdutil.OptionString(opts, "workflow-path"),
- outputPathInput: cmdutil.OptionString(opts, "outputPath"),
- outputPathHyphenInput: cmdutil.OptionString(opts, "output-path"),
- outputPathSnakeInput: cmdutil.OptionString(opts, "output_path"),
- legacyOutputInput: cmdutil.OptionString(opts, "output"),
- workflowOutputPathInput: cmdutil.OptionString(opts, "workflowOutputPath"),
- workflowOutputSnakeInput: cmdutil.OptionString(opts, "workflow_output"),
- workflowOutputHyphenInput: cmdutil.OptionString(opts, "workflow-output"),
- workflowOutputPathHyphenInput: cmdutil.OptionString(opts, "workflow-output-path"),
- workflowOutputPathSnakeInput: cmdutil.OptionString(opts, "workflow_output_path"),
- })
- },
- })
-}
-
-// runReleaseWorkflow writes the embedded release workflow into the current
-// project directory.
-//
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{}) // writes .github/workflows/release.yml
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{pathInput: "ci/release.yml"}) // writes ./ci/release.yml under the project root
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{workflowPathInput: "ci/release.yml"}) // uses the workflowPath alias
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{workflowPathSnakeInput: "ci/release.yml"}) // uses the workflow_path alias
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{workflowPathHyphenInput: "ci/release.yml"}) // uses the workflow-path alias
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{outputPathInput: "ci/release.yml"}) // uses the outputPath alias
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{legacyOutputInput: "ci/release.yml"}) // uses the legacy output alias
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{workflowOutputPathInput: "ci/release.yml"}) // uses the workflowOutputPath alias
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{workflowOutputHyphenInput: "ci/release.yml"}) // uses the workflow-output alias
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{workflowOutputSnakeInput: "ci/release.yml"}) // uses the workflow_output alias
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{workflowOutputPathSnakeInput: "ci/release.yml"}) // uses the workflow_output_path alias
-// runReleaseWorkflow(ctx, releaseWorkflowRequestInputs{workflowOutputPathHyphenInput: "ci/release.yml"}) // uses the workflow-output-path alias
-func runReleaseWorkflow(_ context.Context, inputs releaseWorkflowRequestInputs) core.Result {
- projectDirResult := ax.Getwd()
- if !projectDirResult.OK {
- return core.Fail(core.E("build.runReleaseWorkflow", "failed to get working directory", core.NewError(projectDirResult.Error())))
- }
- projectDir := projectDirResult.Value.(string)
-
- resolvedWorkflowPath := inputs.resolveReleaseWorkflowTargetPath(projectDir, storage.Local)
- if !resolvedWorkflowPath.OK {
- return resolvedWorkflowPath
- }
-
- return build.WriteReleaseWorkflow(storage.Local, resolvedWorkflowPath.Value.(string))
-}
-
-// resolveReleaseWorkflowInputPathAliases("/tmp/project", "ci/release.yml", "", "", "") // "/tmp/project/ci/release.yml"
-// resolveReleaseWorkflowInputPathAliases("/tmp/project", "", "ci/release.yml", "", "") // "/tmp/project/ci/release.yml"
-func resolveReleaseWorkflowInputPathAliases(projectDir, pathInput, workflowPathInput, workflowPathSnakeInput, workflowPathHyphenInput string) core.Result {
- resolvedWorkflowPath := build.ResolveReleaseWorkflowInputPathAliases(
- storage.Local,
- projectDir,
- pathInput,
- workflowPathInput,
- workflowPathSnakeInput,
- workflowPathHyphenInput,
- )
- if !resolvedWorkflowPath.OK {
- return core.Fail(core.E("build.runReleaseWorkflow", "workflow path aliases specify different locations", nil))
- }
-
- return resolvedWorkflowPath
-}
-
-// resolveReleaseWorkflowOutputPathAliases("/tmp/project", "ci/release.yml", "", "", "", "", "", "", "", "") // "/tmp/project/ci/release.yml"
-// resolveReleaseWorkflowOutputPathAliases("/tmp/project", "", "", "", "", "ci/release.yml", "", "", "", "") // "/tmp/project/ci/release.yml"
-func resolveReleaseWorkflowOutputPathAliases(projectDir, outputPathInput, outputPathHyphenInput, outputPathSnakeInput, legacyOutputInput, workflowOutputPathInput, workflowOutputSnakeInput, workflowOutputHyphenInput, workflowOutputPathSnakeInput, workflowOutputPathHyphenInput string) core.Result {
- resolvedWorkflowOutputPath := build.ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(
- storage.Local,
- projectDir,
- outputPathInput,
- outputPathHyphenInput,
- outputPathSnakeInput,
- legacyOutputInput,
- workflowOutputPathInput,
- workflowOutputSnakeInput,
- workflowOutputHyphenInput,
- workflowOutputPathSnakeInput,
- workflowOutputPathHyphenInput,
- )
- if !resolvedWorkflowOutputPath.OK {
- return core.Fail(core.E("build.runReleaseWorkflow", "workflow output aliases specify different locations", nil))
- }
-
- return resolvedWorkflowOutputPath
-}
-
-// runReleaseWorkflowInDir writes the embedded release workflow into projectDir.
-//
-// runReleaseWorkflowInDir("/tmp/project", "", "") // /tmp/project/.github/workflows/release.yml
-// runReleaseWorkflowInDir("/tmp/project", "ci/release.yml", "") // /tmp/project/ci/release.yml
-// runReleaseWorkflowInDir("/tmp/project", ".github/workflows", "") // /tmp/project/.github/workflows/release.yml
-func runReleaseWorkflowInDir(projectDir, workflowPathInput, workflowOutputPathInput string) core.Result {
- resolvedPath := build.ResolveReleaseWorkflowInputPathWithMedium(storage.Local, projectDir, workflowPathInput, workflowOutputPathInput)
- if !resolvedPath.OK {
- return resolvedPath
- }
-
- return build.WriteReleaseWorkflow(storage.Local, resolvedPath.Value.(string))
-}
diff --git a/cmd/build/cmd_workflow_example_test.go b/cmd/build/cmd_workflow_example_test.go
deleted file mode 100644
index f0cab11..0000000
--- a/cmd/build/cmd_workflow_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package buildcmd
-
-import core "dappco.re/go"
-
-// ExampleAddWorkflowCommand references AddWorkflowCommand on this package API surface.
-func ExampleAddWorkflowCommand() {
- _ = AddWorkflowCommand
- core.Println("AddWorkflowCommand")
- // Output: AddWorkflowCommand
-}
diff --git a/cmd/build/cmd_workflow_test.go b/cmd/build/cmd_workflow_test.go
deleted file mode 100644
index bf247d9..0000000
--- a/cmd/build/cmd_workflow_test.go
+++ /dev/null
@@ -1,344 +0,0 @@
-package buildcmd
-
-import (
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/buildtest"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathInputGood(t *testing.T) {
- t.Run("accepts the preferred output path", func(t *testing.T) {
- path := requireBuildCmdString(t, build.ResolveReleaseWorkflowOutputPath("ci/release.yml", "", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the snake_case output path alias", func(t *testing.T) {
- path := requireBuildCmdString(t, build.ResolveReleaseWorkflowOutputPath("", "ci/release.yml", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the legacy output alias", func(t *testing.T) {
- path := requireBuildCmdString(t, build.ResolveReleaseWorkflowOutputPath("", "", "ci/release.yml"))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts matching output aliases", func(t *testing.T) {
- path := requireBuildCmdString(t, build.ResolveReleaseWorkflowOutputPath("ci/release.yml", "ci/release.yml", "ci/release.yml"))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathInputBad(t *testing.T) {
- message := requireBuildCmdError(t, build.ResolveReleaseWorkflowOutputPath("ci/release.yml", "ops/release.yml", ""))
- if !stdlibAssertContains(message, "output aliases specify different locations") {
- t.Fatalf("expected %v to contain %v", message, "output aliases specify different locations")
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathAliases_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowOutputPathAliases(projectDir, "ci/release.yml", "", "", "", "", "./ci/release.yml", "ci/release.yml", "", ""))
- if !stdlibAssertEqual(ax.Join(projectDir, "ci", "release.yml"), path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "ci", "release.yml"), path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathAliases_CamelCaseGood(t *testing.T) {
- projectDir := t.TempDir()
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowOutputPathAliases(projectDir, "ci/release.yml", "", "", "", "", "", "", "", ""))
- if !stdlibAssertEqual(ax.Join(projectDir, "ci", "release.yml"), path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "ci", "release.yml"), path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathAliases_WorkflowCamelCaseGood(t *testing.T) {
- projectDir := t.TempDir()
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowOutputPathAliases(projectDir, "", "", "", "", "ci/release.yml", "", "", "", ""))
- if !stdlibAssertEqual(ax.Join(projectDir, "ci", "release.yml"), path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "ci", "release.yml"), path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathAliases_WorkflowHyphenGood(t *testing.T) {
- projectDir := t.TempDir()
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowOutputPathAliases(projectDir, "", "", "", "", "", "ci/release.yml", "", "", ""))
- if !stdlibAssertEqual(ax.Join(projectDir, "ci", "release.yml"), path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "ci", "release.yml"), path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathAliases_WorkflowSnakeGood(t *testing.T) {
- projectDir := t.TempDir()
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowOutputPathAliases(projectDir, "", "", "", "", "", "", "ci/release.yml", "", ""))
- if !stdlibAssertEqual(ax.Join(projectDir, "ci", "release.yml"), path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "ci", "release.yml"), path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathAliases_Bad(t *testing.T) {
- projectDir := t.TempDir()
-
- message := requireBuildCmdError(t, resolveReleaseWorkflowOutputPathAliases(projectDir, "ci/release.yml", "", "", "", "ops/release.yml", "", "", "", ""))
- if !stdlibAssertContains(message, "workflow output aliases specify different locations") {
- t.Fatalf("expected %v to contain %v", message, "workflow output aliases specify different locations")
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathAliases_HyphenatedGood(t *testing.T) {
- projectDir := t.TempDir()
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowOutputPathAliases(projectDir, "", "ci/release.yml", "", "", "", "", "", "", ""))
- if !stdlibAssertEqual(ax.Join(projectDir, "ci", "release.yml"), path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "ci", "release.yml"), path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathAliases_AbsoluteEquivalent_Good(t *testing.T) {
- projectDir := t.TempDir()
- absolutePath := ax.Join(projectDir, "ci", "release.yml")
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowOutputPathAliases(projectDir, "ci/release.yml", "", "", "", "", "", "", "", absolutePath))
- if !stdlibAssertEqual(absolutePath, path) {
- t.Fatalf("want %v, got %v", absolutePath, path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowOutputPathAliases_AbsoluteDirectory_Good(t *testing.T) {
- projectDir := t.TempDir()
- absoluteDir := ax.Join(projectDir, "ops")
- requireBuildCmdOK(t, storage.Local.EnsureDir(absoluteDir))
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowOutputPathAliases(projectDir, "", "", "", "", absoluteDir, "", "", "", ""))
- if !stdlibAssertEqual(ax.Join(absoluteDir, "release.yml"), path) {
- t.Fatalf("want %v, got %v", ax.Join(absoluteDir, "release.yml"), path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowInputPathAliases_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowInputPathAliases(projectDir, "ci/release.yml", "", "", ""))
- if !stdlibAssertEqual(ax.Join(projectDir, "ci", "release.yml"), path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "ci", "release.yml"), path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowInputPathAliases_WorkflowPathGood(t *testing.T) {
- projectDir := t.TempDir()
-
- path := requireBuildCmdString(t, resolveReleaseWorkflowInputPathAliases(projectDir, "", "ci/release.yml", "", ""))
- if !stdlibAssertEqual(ax.Join(projectDir, "ci", "release.yml"), path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "ci", "release.yml"), path)
- }
-
-}
-
-func TestBuildCmd_resolveReleaseWorkflowInputPathAliases_Bad(t *testing.T) {
- projectDir := t.TempDir()
-
- message := requireBuildCmdError(t, resolveReleaseWorkflowInputPathAliases(projectDir, "ci/release.yml", "ops/release.yml", "", ""))
- if !stdlibAssertContains(message, "workflow path aliases specify different locations") {
- t.Fatalf("expected %v to contain %v", message, "workflow path aliases specify different locations")
- }
-
-}
-
-func TestBuildCmd_RunReleaseWorkflowGood(t *testing.T) {
- projectDir := t.TempDir()
-
- t.Run("writes to the conventional workflow path by default", func(t *testing.T) {
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "", ""))
-
- path := build.ReleaseWorkflowPath(projectDir)
- content := requireBuildCmdString(t, storage.Local.Read(path))
- buildtest.AssertReleaseWorkflowContent(t, content)
-
- })
-
- t.Run("registers the build/workflow command", func(t *testing.T) {
- c := core.New()
- AddWorkflowCommand(c)
-
- result := c.Command("build/workflow")
- if !(result.OK) {
- t.Fatal("expected true")
- }
-
- command, ok := result.Value.(*core.Command)
- if !(ok) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("build/workflow", command.Path) {
- t.Fatalf("want %v, got %v", "build/workflow", command.Path)
- }
- if !stdlibAssertEqual("cmd.build.workflow.long", command.Description) {
- t.Fatalf("want %v, got %v", "cmd.build.workflow.long", command.Description)
- }
-
- })
-
- t.Run("writes to a custom relative path", func(t *testing.T) {
- customPath := "ci/release.yml"
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, customPath, ""))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, customPath)))
- buildtest.AssertReleaseWorkflowContent(t, content)
-
- })
-
- t.Run("writes release.yml inside a directory-style relative path", func(t *testing.T) {
- customPath := "ci/"
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, customPath, ""))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, "ci", "release.yml")))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes release.yml inside an existing directory without a trailing slash", func(t *testing.T) {
- requireBuildCmdOK(t, storage.Local.EnsureDir(ax.Join(projectDir, "ops")))
-
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "ops", ""))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, "ops", "release.yml")))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes release.yml inside a bare directory-style path", func(t *testing.T) {
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "ci", ""))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, "ci", "release.yml")))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes release.yml inside a current-directory-prefixed directory-style path", func(t *testing.T) {
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "./ci", ""))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, "ci", "release.yml")))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes release.yml inside the conventional workflows directory", func(t *testing.T) {
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, ".github/workflows", ""))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, ".github", "workflows", "release.yml")))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes release.yml inside a current-directory-prefixed workflows directory", func(t *testing.T) {
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "./.github/workflows", ""))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, ".github", "workflows", "release.yml")))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes to the output alias", func(t *testing.T) {
- customPath := "ci/alias-release.yml"
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "", customPath))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, customPath)))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes to the output-path alias", func(t *testing.T) {
- customPath := "ci/output-path-release.yml"
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "", customPath))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, customPath)))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes to the output_path alias", func(t *testing.T) {
- customPath := "ci/output_path-release.yml"
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "", customPath))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, customPath)))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes to the workflow-output alias", func(t *testing.T) {
- customPath := "ci/workflow-output-release.yml"
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "", customPath))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, customPath)))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-
- t.Run("writes to the workflow_output alias", func(t *testing.T) {
- customPath := "ci/workflow_output-release.yml"
- requireBuildCmdOK(t, runReleaseWorkflowInDir(projectDir, "", customPath))
-
- content := requireBuildCmdString(t, storage.Local.Read(ax.Join(projectDir, customPath)))
- buildtest.AssertReleaseWorkflowTriggers(t, content)
-
- })
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCmdWorkflow_AddWorkflowCommand_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- AddWorkflowCommand(core.New())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCmdWorkflow_AddWorkflowCommand_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- AddWorkflowCommand(core.New())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCmdWorkflow_AddWorkflowCommand_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- AddWorkflowCommand(core.New())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/cmd/build/tmpl/gui/go.mod.tmpl b/cmd/build/tmpl/gui/go.mod.tmpl
deleted file mode 100644
index 05f0d17..0000000
--- a/cmd/build/tmpl/gui/go.mod.tmpl
+++ /dev/null
@@ -1,7 +0,0 @@
-module {{.AppModule}}
-
-go 1.21
-
-require (
- github.com/wailsapp/wails/v3 v3.0.0-alpha.8
-)
diff --git a/cmd/build/tmpl/gui/html/.gitkeep b/cmd/build/tmpl/gui/html/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/cmd/build/tmpl/gui/html/.placeholder b/cmd/build/tmpl/gui/html/.placeholder
deleted file mode 100644
index 1044078..0000000
--- a/cmd/build/tmpl/gui/html/.placeholder
+++ /dev/null
@@ -1 +0,0 @@
-// This file ensures the 'html' directory is correctly embedded by the Go compiler.
diff --git a/cmd/build/tmpl/gui/main.go.tmpl b/cmd/build/tmpl/gui/main.go.tmpl
deleted file mode 100644
index bc5daef..0000000
--- a/cmd/build/tmpl/gui/main.go.tmpl
+++ /dev/null
@@ -1,25 +0,0 @@
-package main
-
-import (
- "embed"
- "log"
-
- "github.com/wailsapp/wails/v3/pkg/application"
-)
-
-//go:embed all:html
-var assets embed.FS
-
-func main() {
- app := application.New(application.Options{
- Name: {{.AppDisplayNameLiteral}},
- Description: {{.AppDescriptionLiteral}},
- Assets: application.AssetOptions{
- FS: assets,
- },
- })
-
- if err := app.Run(); err != nil {
- log.Fatal(err)
- }
-}
diff --git a/cmd/ci/stdlib_assert_test.go b/cmd/ci/stdlib_assert_test.go
deleted file mode 100644
index 3af8c93..0000000
--- a/cmd/ci/stdlib_assert_test.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package ci
-
-import "dappco.re/go/build/internal/testassert"
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
diff --git a/cmd/sdk/stdlib_assert_test.go b/cmd/sdk/stdlib_assert_test.go
deleted file mode 100644
index 45c0fab..0000000
--- a/cmd/sdk/stdlib_assert_test.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package sdkcmd
-
-import "dappco.re/go/build/internal/testassert"
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
diff --git a/external/go b/external/go
new file mode 160000
index 0000000..d661b70
--- /dev/null
+++ b/external/go
@@ -0,0 +1 @@
+Subproject commit d661b703e16183b3cbab101de189f688888a1174
diff --git a/go.work b/go.work
new file mode 100644
index 0000000..3d4907c
--- /dev/null
+++ b/go.work
@@ -0,0 +1,9 @@
+go 1.26.2
+
+// Workspace mode for development: pulls fresh code from external/ submodules.
+// CI uses GOWORK=off to fall back to go/go.mod tags (reproducible).
+
+use (
+ ./go
+ ./external/go
+)
diff --git a/go.work.sum b/go.work.sum
new file mode 100644
index 0000000..c7cdeb0
--- /dev/null
+++ b/go.work.sum
@@ -0,0 +1,224 @@
+cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
+cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
+cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8=
+cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
+cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
+cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
+cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
+cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
+cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
+cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
+cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
+github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
+github.com/Snider/Enchantrix v0.0.2 h1:ExZQiBhfS/p/AHFTKhY80TOd+BXZjK95EzByAEgwvjs=
+github.com/Snider/Enchantrix v0.0.2/go.mod h1:CtFcLAvnDT1KcuF1JBb/DJj0KplY8jHryO06KzQ1hsQ=
+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
+github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
+github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
+github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
+github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
+github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
+github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
+github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
+github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
+github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8=
+github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
+github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
+github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
+github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
+github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
+github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
+github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
+github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e h1:a+PGEeXb+exwBS3NboqXHyxarD9kaboBbrSp+7GuBuc=
+github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
+github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
+github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
+github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
+github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
+github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
+github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
+github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
+github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
+github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
+github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
+github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
+github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
+github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
+github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
+github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
+github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
+github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
+github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
+github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
+github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
+github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
+github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
+github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
+github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
+github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
+github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
+go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
+go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
+go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
+go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
+go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
+go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
+go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
+go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
+go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
+go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
+go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
+golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
+golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
+golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
+golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
+golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
+google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc=
+google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
+google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 h1:mVXdvnmR3S3BQOqHECm9NGMjYiRtEvDYcqAqedTXY6s=
+google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:vYFwMYFbmA8vl6Z/krj/h7+U/AqpHknwJX4Uqgfyc7I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/go/AGENTS.md b/go/AGENTS.md
new file mode 120000
index 0000000..be77ac8
--- /dev/null
+++ b/go/AGENTS.md
@@ -0,0 +1 @@
+../AGENTS.md
\ No newline at end of file
diff --git a/go/CLAUDE.md b/go/CLAUDE.md
new file mode 120000
index 0000000..949a29f
--- /dev/null
+++ b/go/CLAUDE.md
@@ -0,0 +1 @@
+../CLAUDE.md
\ No newline at end of file
diff --git a/go/README.md b/go/README.md
new file mode 120000
index 0000000..32d46ee
--- /dev/null
+++ b/go/README.md
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/cmd/ci/ci.go b/go/cmd/ci/ci.go
similarity index 97%
rename from cmd/ci/ci.go
rename to go/cmd/ci/ci.go
index 14c093e..2e5b2da 100644
--- a/cmd/ci/ci.go
+++ b/go/cmd/ci/ci.go
@@ -20,7 +20,7 @@ var (
)
func registerCICommands(c *core.Core) {
- c.Command("ci", core.Command{
+ _ = c.Command("ci", core.Command{
Description: "cmd.ci.long",
Action: func(opts core.Options) core.Result {
dryRun := !cmdutil.OptionBool(opts, "we-are-go-for-launch")
@@ -34,14 +34,14 @@ func registerCICommands(c *core.Core) {
},
})
- c.Command("ci/init", core.Command{
+ _ = c.Command("ci/init", core.Command{
Description: "cmd.ci.init.long",
Action: func(opts core.Options) core.Result {
return runCIReleaseInit()
},
})
- c.Command("ci/changelog", core.Command{
+ _ = c.Command("ci/changelog", core.Command{
Description: "cmd.ci.changelog.long",
Action: func(opts core.Options) core.Result {
return runChangelog(
@@ -52,7 +52,7 @@ func registerCICommands(c *core.Core) {
},
})
- c.Command("ci/version", core.Command{
+ _ = c.Command("ci/version", core.Command{
Description: "cmd.ci.version.long",
Action: func(opts core.Options) core.Result {
return runCIReleaseVersion(cmdutil.ContextOrBackground())
diff --git a/cmd/ci/ci_test.go b/go/cmd/ci/ci_test.go
similarity index 100%
rename from cmd/ci/ci_test.go
rename to go/cmd/ci/ci_test.go
diff --git a/cmd/ci/cmd.go b/go/cmd/ci/cmd.go
similarity index 100%
rename from cmd/ci/cmd.go
rename to go/cmd/ci/cmd.go
diff --git a/cmd/ci/cmd_example_test.go b/go/cmd/ci/cmd_example_test.go
similarity index 100%
rename from cmd/ci/cmd_example_test.go
rename to go/cmd/ci/cmd_example_test.go
diff --git a/cmd/ci/cmd_test.go b/go/cmd/ci/cmd_test.go
similarity index 100%
rename from cmd/ci/cmd_test.go
rename to go/cmd/ci/cmd_test.go
diff --git a/go/cmd/ci/stdlib_assert_test.go b/go/cmd/ci/stdlib_assert_test.go
new file mode 100644
index 0000000..b50dec2
--- /dev/null
+++ b/go/cmd/ci/stdlib_assert_test.go
@@ -0,0 +1,7 @@
+package ci
+
+import "dappco.re/go/build/internal/testassert"
+
+var (
+ stdlibAssertContains = testassert.Contains
+)
diff --git a/cmd/sdk/cmd.go b/go/cmd/sdk/cmd.go
similarity index 98%
rename from cmd/sdk/cmd.go
rename to go/cmd/sdk/cmd.go
index 82a15d8..455e686 100644
--- a/cmd/sdk/cmd.go
+++ b/go/cmd/sdk/cmd.go
@@ -37,7 +37,7 @@ func AddSDKCommands(c *core.Core) {
registerSDKGenerateCommand(c, "sdk")
registerSDKGenerateCommand(c, "sdk/generate")
- c.Command("sdk/diff", core.Command{
+ _ = c.Command("sdk/diff", core.Command{
Description: "cmd.sdk.diff.long",
Action: func(opts core.Options) core.Result {
return runSDKDiff(
@@ -48,7 +48,7 @@ func AddSDKCommands(c *core.Core) {
},
})
- c.Command("sdk/validate", core.Command{
+ _ = c.Command("sdk/validate", core.Command{
Description: "cmd.sdk.validate.long",
Action: func(opts core.Options) core.Result {
return runSDKValidate(
@@ -59,7 +59,7 @@ func AddSDKCommands(c *core.Core) {
}
func registerSDKGenerateCommand(c *core.Core, path string) {
- c.Command(path, core.Command{
+ _ = c.Command(path, core.Command{
Description: "cmd.sdk.long",
Action: func(opts core.Options) core.Result {
return runSDKGenerate(
diff --git a/cmd/sdk/cmd_example_test.go b/go/cmd/sdk/cmd_example_test.go
similarity index 100%
rename from cmd/sdk/cmd_example_test.go
rename to go/cmd/sdk/cmd_example_test.go
diff --git a/cmd/sdk/cmd_test.go b/go/cmd/sdk/cmd_test.go
similarity index 100%
rename from cmd/sdk/cmd_test.go
rename to go/cmd/sdk/cmd_test.go
diff --git a/go/cmd/sdk/stdlib_assert_test.go b/go/cmd/sdk/stdlib_assert_test.go
new file mode 100644
index 0000000..77c7103
--- /dev/null
+++ b/go/cmd/sdk/stdlib_assert_test.go
@@ -0,0 +1,8 @@
+package sdkcmd
+
+import "dappco.re/go/build/internal/testassert"
+
+var (
+ stdlibAssertEqual = testassert.Equal
+ stdlibAssertContains = testassert.Contains
+)
diff --git a/cmd/service/cmd.go b/go/cmd/service/cmd.go
similarity index 93%
rename from cmd/service/cmd.go
rename to go/cmd/service/cmd.go
index 21e4836..a06f3bb 100644
--- a/cmd/service/cmd.go
+++ b/go/cmd/service/cmd.go
@@ -29,49 +29,49 @@ type serviceRequest = servicecommon.Request
// AddServiceCommands registers `core service` commands.
func AddServiceCommands(c *core.Core) {
- c.Command("service", core.Command{
+ _ = c.Command("service", core.Command{
Description: "cmd.service.long",
Action: func(opts core.Options) core.Result {
return core.Fail(core.E("service", "use a subcommand: install, start, stop, uninstall, export", nil))
},
})
- c.Command("service/install", core.Command{
+ _ = c.Command("service/install", core.Command{
Description: "cmd.service.install.long",
Action: func(opts core.Options) core.Result {
return runServiceInstall(requestFromOptions(opts))
},
})
- c.Command("service/start", core.Command{
+ _ = c.Command("service/start", core.Command{
Description: "cmd.service.start.long",
Action: func(opts core.Options) core.Result {
return runServiceStart(requestFromOptions(opts))
},
})
- c.Command("service/stop", core.Command{
+ _ = c.Command("service/stop", core.Command{
Description: "cmd.service.stop.long",
Action: func(opts core.Options) core.Result {
return runServiceStop(requestFromOptions(opts))
},
})
- c.Command("service/uninstall", core.Command{
+ _ = c.Command("service/uninstall", core.Command{
Description: "cmd.service.uninstall.long",
Action: func(opts core.Options) core.Result {
return runServiceUninstall(requestFromOptions(opts))
},
})
- c.Command("service/export", core.Command{
+ _ = c.Command("service/export", core.Command{
Description: "cmd.service.export.long",
Action: func(opts core.Options) core.Result {
return runServiceExport(requestFromOptions(opts))
},
})
- c.Command("service/run", core.Command{
+ _ = c.Command("service/run", core.Command{
Description: "cmd.service.run.long",
Hidden: true,
Action: func(opts core.Options) core.Result {
@@ -202,7 +202,3 @@ func loadServiceConfig(req serviceRequest) core.Result {
func applyServiceOverrides(cfg *buildservice.Config, req serviceRequest) core.Result {
return servicecommon.ApplyOverrides(cfg, req)
}
-
-func parseCSV(value string) []string {
- return servicecommon.ParseCSV(value)
-}
diff --git a/cmd/service/cmd_example_test.go b/go/cmd/service/cmd_example_test.go
similarity index 100%
rename from cmd/service/cmd_example_test.go
rename to go/cmd/service/cmd_example_test.go
diff --git a/cmd/service/cmd_test.go b/go/cmd/service/cmd_test.go
similarity index 100%
rename from cmd/service/cmd_test.go
rename to go/cmd/service/cmd_test.go
diff --git a/cmd/service/stdlib_assert_test.go b/go/cmd/service/stdlib_assert_test.go
similarity index 82%
rename from cmd/service/stdlib_assert_test.go
rename to go/cmd/service/stdlib_assert_test.go
index 87394da..2e6f5d0 100644
--- a/cmd/service/stdlib_assert_test.go
+++ b/go/cmd/service/stdlib_assert_test.go
@@ -7,11 +7,7 @@ import (
var (
stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
)
type serviceCmdFatal interface {
diff --git a/go/docs b/go/docs
new file mode 120000
index 0000000..a9594bf
--- /dev/null
+++ b/go/docs
@@ -0,0 +1 @@
+../docs
\ No newline at end of file
diff --git a/go.mod b/go/go.mod
similarity index 100%
rename from go.mod
rename to go/go.mod
diff --git a/go.sum b/go/go.sum
similarity index 100%
rename from go.sum
rename to go/go.sum
diff --git a/internal/ax/ax.go b/go/internal/ax/ax.go
similarity index 100%
rename from internal/ax/ax.go
rename to go/internal/ax/ax.go
diff --git a/internal/ax/ax_example_test.go b/go/internal/ax/ax_example_test.go
similarity index 100%
rename from internal/ax/ax_example_test.go
rename to go/internal/ax/ax_example_test.go
diff --git a/internal/ax/ax_test.go b/go/internal/ax/ax_test.go
similarity index 100%
rename from internal/ax/ax_test.go
rename to go/internal/ax/ax_test.go
diff --git a/internal/buildtest/workflow.go b/go/internal/buildtest/workflow.go
similarity index 100%
rename from internal/buildtest/workflow.go
rename to go/internal/buildtest/workflow.go
diff --git a/internal/buildtest/workflow_example_test.go b/go/internal/buildtest/workflow_example_test.go
similarity index 100%
rename from internal/buildtest/workflow_example_test.go
rename to go/internal/buildtest/workflow_example_test.go
diff --git a/internal/buildtest/workflow_test.go b/go/internal/buildtest/workflow_test.go
similarity index 100%
rename from internal/buildtest/workflow_test.go
rename to go/internal/buildtest/workflow_test.go
diff --git a/internal/cli/cli.go b/go/internal/cli/cli.go
similarity index 96%
rename from internal/cli/cli.go
rename to go/internal/cli/cli.go
index 90c6888..7931f51 100644
--- a/internal/cli/cli.go
+++ b/go/internal/cli/cli.go
@@ -22,7 +22,6 @@ var (
var (
stdout io.Writer = core.Stdout()
- stderr io.Writer = core.Stderr()
)
func SetStdout(w io.Writer) {
@@ -35,10 +34,8 @@ func SetStdout(w io.Writer) {
func SetStderr(w io.Writer) {
if w == nil {
- stderr = core.Stderr()
return
}
- stderr = w
}
func Print(format string, args ...any) {
diff --git a/internal/cli/cli_example_test.go b/go/internal/cli/cli_example_test.go
similarity index 100%
rename from internal/cli/cli_example_test.go
rename to go/internal/cli/cli_example_test.go
diff --git a/internal/cli/cli_test.go b/go/internal/cli/cli_test.go
similarity index 99%
rename from internal/cli/cli_test.go
rename to go/internal/cli/cli_test.go
index 32b000e..e850cd2 100644
--- a/internal/cli/cli_test.go
+++ b/go/internal/cli/cli_test.go
@@ -49,7 +49,7 @@ func TestCli_SetStdout_Ugly(t *core.T) {
func TestCli_SetStderr_Good(t *core.T) {
buffer := core.NewBuffer()
SetStderr(buffer)
- Err("%s", "stderr")
+ _ = Err("%s", "stderr")
core.AssertEqual(t, "", buffer.String())
}
diff --git a/internal/cmdutil/cmdutil.go b/go/internal/cmdutil/cmdutil.go
similarity index 100%
rename from internal/cmdutil/cmdutil.go
rename to go/internal/cmdutil/cmdutil.go
diff --git a/internal/cmdutil/cmdutil_example_test.go b/go/internal/cmdutil/cmdutil_example_test.go
similarity index 100%
rename from internal/cmdutil/cmdutil_example_test.go
rename to go/internal/cmdutil/cmdutil_example_test.go
diff --git a/internal/cmdutil/cmdutil_test.go b/go/internal/cmdutil/cmdutil_test.go
similarity index 100%
rename from internal/cmdutil/cmdutil_test.go
rename to go/internal/cmdutil/cmdutil_test.go
diff --git a/internal/projectdetect/projectdetect.go b/go/internal/projectdetect/projectdetect.go
similarity index 100%
rename from internal/projectdetect/projectdetect.go
rename to go/internal/projectdetect/projectdetect.go
diff --git a/internal/projectdetect/projectdetect_example_test.go b/go/internal/projectdetect/projectdetect_example_test.go
similarity index 100%
rename from internal/projectdetect/projectdetect_example_test.go
rename to go/internal/projectdetect/projectdetect_example_test.go
diff --git a/internal/projectdetect/projectdetect_test.go b/go/internal/projectdetect/projectdetect_test.go
similarity index 100%
rename from internal/projectdetect/projectdetect_test.go
rename to go/internal/projectdetect/projectdetect_test.go
diff --git a/go/internal/projectdetect/stdlib_assert_test.go b/go/internal/projectdetect/stdlib_assert_test.go
new file mode 100644
index 0000000..68a69df
--- /dev/null
+++ b/go/internal/projectdetect/stdlib_assert_test.go
@@ -0,0 +1,8 @@
+package projectdetect
+
+import "dappco.re/go/build/internal/testassert"
+
+var (
+ stdlibAssertEqual = testassert.Equal
+ stdlibAssertEmpty = testassert.Empty
+)
diff --git a/internal/sdkcfg/sdkcfg.go b/go/internal/sdkcfg/sdkcfg.go
similarity index 100%
rename from internal/sdkcfg/sdkcfg.go
rename to go/internal/sdkcfg/sdkcfg.go
diff --git a/internal/sdkcfg/sdkcfg_example_test.go b/go/internal/sdkcfg/sdkcfg_example_test.go
similarity index 100%
rename from internal/sdkcfg/sdkcfg_example_test.go
rename to go/internal/sdkcfg/sdkcfg_example_test.go
diff --git a/internal/sdkcfg/sdkcfg_test.go b/go/internal/sdkcfg/sdkcfg_test.go
similarity index 100%
rename from internal/sdkcfg/sdkcfg_test.go
rename to go/internal/sdkcfg/sdkcfg_test.go
diff --git a/go/internal/sdkcfg/stdlib_assert_test.go b/go/internal/sdkcfg/stdlib_assert_test.go
new file mode 100644
index 0000000..9c1a08d
--- /dev/null
+++ b/go/internal/sdkcfg/stdlib_assert_test.go
@@ -0,0 +1,8 @@
+package sdkcfg
+
+import "dappco.re/go/build/internal/testassert"
+
+var (
+ stdlibAssertEqual = testassert.Equal
+ stdlibAssertNil = testassert.Nil
+)
diff --git a/internal/servicecmd/request.go b/go/internal/servicecmd/request.go
similarity index 100%
rename from internal/servicecmd/request.go
rename to go/internal/servicecmd/request.go
diff --git a/internal/servicecmd/request_example_test.go b/go/internal/servicecmd/request_example_test.go
similarity index 100%
rename from internal/servicecmd/request_example_test.go
rename to go/internal/servicecmd/request_example_test.go
diff --git a/internal/servicecmd/request_test.go b/go/internal/servicecmd/request_test.go
similarity index 100%
rename from internal/servicecmd/request_test.go
rename to go/internal/servicecmd/request_test.go
diff --git a/internal/testassert/testassert.go b/go/internal/testassert/testassert.go
similarity index 100%
rename from internal/testassert/testassert.go
rename to go/internal/testassert/testassert.go
diff --git a/internal/testassert/testassert_example_test.go b/go/internal/testassert/testassert_example_test.go
similarity index 100%
rename from internal/testassert/testassert_example_test.go
rename to go/internal/testassert/testassert_example_test.go
diff --git a/internal/testassert/testassert_test.go b/go/internal/testassert/testassert_test.go
similarity index 100%
rename from internal/testassert/testassert_test.go
rename to go/internal/testassert/testassert_test.go
diff --git a/locales/embed.go b/go/locales/embed.go
similarity index 100%
rename from locales/embed.go
rename to go/locales/embed.go
diff --git a/locales/en.json b/go/locales/en.json
similarity index 100%
rename from locales/en.json
rename to go/locales/en.json
diff --git a/pkg/api/embed.go b/go/pkg/api/embed.go
similarity index 100%
rename from pkg/api/embed.go
rename to go/pkg/api/embed.go
diff --git a/pkg/api/http.go b/go/pkg/api/http.go
similarity index 100%
rename from pkg/api/http.go
rename to go/pkg/api/http.go
diff --git a/pkg/api/http_example_test.go b/go/pkg/api/http_example_test.go
similarity index 100%
rename from pkg/api/http_example_test.go
rename to go/pkg/api/http_example_test.go
diff --git a/pkg/api/http_test.go b/go/pkg/api/http_test.go
similarity index 100%
rename from pkg/api/http_test.go
rename to go/pkg/api/http_test.go
diff --git a/pkg/api/provider.go b/go/pkg/api/provider.go
similarity index 99%
rename from pkg/api/provider.go
rename to go/pkg/api/provider.go
index cf9b48f..01ec3f5 100644
--- a/pkg/api/provider.go
+++ b/go/pkg/api/provider.go
@@ -56,7 +56,6 @@ const (
)
var (
- providerResolveProjectType = resolveProjectType
providerGetBuilder = getBuilder
providerDetermineVersion = release.DetermineVersionWithContext
providerLoadReleaseConfig = release.LoadConfig
diff --git a/pkg/api/provider/provider.go b/go/pkg/api/provider/provider.go
similarity index 100%
rename from pkg/api/provider/provider.go
rename to go/pkg/api/provider/provider.go
diff --git a/pkg/api/provider/provider_example_test.go b/go/pkg/api/provider/provider_example_test.go
similarity index 100%
rename from pkg/api/provider/provider_example_test.go
rename to go/pkg/api/provider/provider_example_test.go
diff --git a/pkg/api/provider/provider_test.go b/go/pkg/api/provider/provider_test.go
similarity index 100%
rename from pkg/api/provider/provider_test.go
rename to go/pkg/api/provider/provider_test.go
diff --git a/pkg/api/provider_example_test.go b/go/pkg/api/provider_example_test.go
similarity index 100%
rename from pkg/api/provider_example_test.go
rename to go/pkg/api/provider_example_test.go
diff --git a/pkg/api/provider_test.go b/go/pkg/api/provider_test.go
similarity index 99%
rename from pkg/api/provider_test.go
rename to go/pkg/api/provider_test.go
index c128bbe..32a9c30 100644
--- a/pkg/api/provider_test.go
+++ b/go/pkg/api/provider_test.go
@@ -445,7 +445,7 @@ func TestProvider_BuildProviderResolveDirGood(t *testing.T) {
func TestProvider_BuildProviderResolveDirRelativeGood(t *testing.T) {
p := NewProvider(".", nil)
dir := requireProviderString(t, p.resolveDir())
- if !(len(dir) > 1 && dir[0] == '/') {
+ if len(dir) <= 1 || dir[0] != '/' {
t.Fatal("expected true")
}
@@ -513,18 +513,15 @@ func TestProvider_StreamEvents_UsesHubHandlerGood(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
- defer conn.Close()
+ defer func() {
+ _ = conn.Close()
+ }()
if err := conn.WriteJSON(events.Message{Type: events.TypeSubscribe, Data: "build.complete"}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
{
deadline := time.Now().Add(time.Second)
- for {
- if (func() bool {
- return hub.ChannelSubscriberCount("build.complete") == 1
- })() {
- break
- }
+ for hub.ChannelSubscriberCount("build.complete") != 1 {
if time.Now().After(deadline) {
t.Fatal("condition was not satisfied")
}
@@ -1260,7 +1257,7 @@ sign:
if stdlibAssertContains(recorder.Body.String(), `"checksum_file"`) {
t.Fatalf("expected %v not to contain %v", recorder.Body.String(), `"checksum_file"`)
}
- if !(storage.Local.Exists(ax.Join(projectDir, "dist", runtime.GOOS+"_"+runtime.GOARCH, "provider")) || storage.Local.Exists(ax.Join(projectDir, "dist", runtime.GOOS+"_"+runtime.GOARCH, "provider.exe"))) {
+ if !storage.Local.Exists(ax.Join(projectDir, "dist", runtime.GOOS+"_"+runtime.GOARCH, "provider")) && !storage.Local.Exists(ax.Join(projectDir, "dist", runtime.GOOS+"_"+runtime.GOARCH, "provider.exe")) {
t.Fatal("expected true")
}
if storage.Local.Exists(ax.Join(projectDir, "dist", "CHECKSUMS.txt")) {
diff --git a/pkg/api/stdlib_assert_test.go b/go/pkg/api/stdlib_assert_test.go
similarity index 90%
rename from pkg/api/stdlib_assert_test.go
rename to go/pkg/api/stdlib_assert_test.go
index ea47736..91768ff 100644
--- a/pkg/api/stdlib_assert_test.go
+++ b/go/pkg/api/stdlib_assert_test.go
@@ -9,10 +9,7 @@ import (
var (
stdlibAssertEqual = testassert.Equal
stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
)
type providerFatal interface {
diff --git a/pkg/events/events.go b/go/pkg/events/events.go
similarity index 100%
rename from pkg/events/events.go
rename to go/pkg/events/events.go
diff --git a/pkg/events/events_example_test.go b/go/pkg/events/events_example_test.go
similarity index 100%
rename from pkg/events/events_example_test.go
rename to go/pkg/events/events_example_test.go
diff --git a/pkg/events/events_test.go b/go/pkg/events/events_test.go
similarity index 100%
rename from pkg/events/events_test.go
rename to go/pkg/events/events_test.go
diff --git a/pkg/release/changelog.go b/go/pkg/release/changelog.go
similarity index 100%
rename from pkg/release/changelog.go
rename to go/pkg/release/changelog.go
diff --git a/pkg/release/changelog_example_test.go b/go/pkg/release/changelog_example_test.go
similarity index 100%
rename from pkg/release/changelog_example_test.go
rename to go/pkg/release/changelog_example_test.go
diff --git a/pkg/release/changelog_test.go b/go/pkg/release/changelog_test.go
similarity index 100%
rename from pkg/release/changelog_test.go
rename to go/pkg/release/changelog_test.go
diff --git a/pkg/release/config.go b/go/pkg/release/config.go
similarity index 100%
rename from pkg/release/config.go
rename to go/pkg/release/config.go
diff --git a/pkg/release/config_example_test.go b/go/pkg/release/config_example_test.go
similarity index 100%
rename from pkg/release/config_example_test.go
rename to go/pkg/release/config_example_test.go
diff --git a/pkg/release/config_test.go b/go/pkg/release/config_test.go
similarity index 100%
rename from pkg/release/config_test.go
rename to go/pkg/release/config_test.go
diff --git a/go/pkg/release/output.go b/go/pkg/release/output.go
new file mode 100644
index 0000000..fcac037
--- /dev/null
+++ b/go/pkg/release/output.go
@@ -0,0 +1,59 @@
+package release
+
+import (
+ "reflect"
+
+ "dappco.re/go/build/internal/ax"
+ coreio "dappco.re/go/build/pkg/storage"
+)
+func resolveReleaseOutputMedium(cfg *Config) coreio.Medium {
+ if cfg == nil || cfg.output == nil {
+ return coreio.Local
+ }
+ return cfg.output
+}
+
+func resolveReleaseOutputRoot(projectDir string, cfg *Config, output coreio.Medium) string {
+ outputDir := ""
+ if cfg != nil {
+ outputDir = cfg.outputDir
+ }
+
+ if outputDir == "" && !mediumEquals(output, coreio.Local) {
+ return ""
+ }
+
+ if outputDir == "" {
+ outputDir = "dist"
+ }
+
+ if !ax.IsAbs(outputDir) && mediumEquals(output, coreio.Local) {
+ return ax.Join(projectDir, outputDir)
+ }
+
+ return outputDir
+}
+
+func joinReleasePath(root, path string) string {
+ if root == "" || root == "." {
+ return ax.Clean(path)
+ }
+ if path == "" || path == "." {
+ return ax.Clean(root)
+ }
+ return ax.Join(root, path)
+}
+
+func mediumEquals(left, right coreio.Medium) bool {
+ if left == nil || right == nil {
+ return left == nil && right == nil
+ }
+
+ leftType := reflect.TypeOf(left)
+ rightType := reflect.TypeOf(right)
+ if leftType != rightType || !leftType.Comparable() {
+ return false
+ }
+
+ return reflect.ValueOf(left).Interface() == reflect.ValueOf(right).Interface()
+}
diff --git a/pkg/release/publishers/assets.go b/go/pkg/release/publishers/assets.go
similarity index 100%
rename from pkg/release/publishers/assets.go
rename to go/pkg/release/publishers/assets.go
diff --git a/pkg/release/publishers/assets_example_test.go b/go/pkg/release/publishers/assets_example_test.go
similarity index 100%
rename from pkg/release/publishers/assets_example_test.go
rename to go/pkg/release/publishers/assets_example_test.go
diff --git a/pkg/release/publishers/assets_test.go b/go/pkg/release/publishers/assets_test.go
similarity index 100%
rename from pkg/release/publishers/assets_test.go
rename to go/pkg/release/publishers/assets_test.go
diff --git a/pkg/release/publishers/aur.go b/go/pkg/release/publishers/aur.go
similarity index 99%
rename from pkg/release/publishers/aur.go
rename to go/pkg/release/publishers/aur.go
index a083e56..6f371f3 100644
--- a/pkg/release/publishers/aur.go
+++ b/go/pkg/release/publishers/aur.go
@@ -274,7 +274,7 @@ func (p *AURPublisher) pushToAUR(ctx context.Context, data aurTemplateData, pkgb
return core.Fail(core.E("aur.pushToAUR", "failed to create temp directory", core.NewError(tmpDirResult.Error())))
}
tmpDir := tmpDirResult.Value.(string)
- defer func() { ax.RemoveAll(tmpDir) }()
+ defer func() { _ = ax.RemoveAll(tmpDir) }()
// Clone existing AUR repo (or initialise new one)
publisherPrint("Cloning AUR package %s-bin...", data.PackageName)
diff --git a/pkg/release/publishers/aur_example_test.go b/go/pkg/release/publishers/aur_example_test.go
similarity index 100%
rename from pkg/release/publishers/aur_example_test.go
rename to go/pkg/release/publishers/aur_example_test.go
diff --git a/pkg/release/publishers/aur_test.go b/go/pkg/release/publishers/aur_test.go
similarity index 100%
rename from pkg/release/publishers/aur_test.go
rename to go/pkg/release/publishers/aur_test.go
diff --git a/pkg/release/publishers/chocolatey.go b/go/pkg/release/publishers/chocolatey.go
similarity index 100%
rename from pkg/release/publishers/chocolatey.go
rename to go/pkg/release/publishers/chocolatey.go
diff --git a/pkg/release/publishers/chocolatey_example_test.go b/go/pkg/release/publishers/chocolatey_example_test.go
similarity index 100%
rename from pkg/release/publishers/chocolatey_example_test.go
rename to go/pkg/release/publishers/chocolatey_example_test.go
diff --git a/pkg/release/publishers/chocolatey_test.go b/go/pkg/release/publishers/chocolatey_test.go
similarity index 100%
rename from pkg/release/publishers/chocolatey_test.go
rename to go/pkg/release/publishers/chocolatey_test.go
diff --git a/pkg/release/publishers/docker.go b/go/pkg/release/publishers/docker.go
similarity index 100%
rename from pkg/release/publishers/docker.go
rename to go/pkg/release/publishers/docker.go
diff --git a/pkg/release/publishers/docker_example_test.go b/go/pkg/release/publishers/docker_example_test.go
similarity index 100%
rename from pkg/release/publishers/docker_example_test.go
rename to go/pkg/release/publishers/docker_example_test.go
diff --git a/pkg/release/publishers/docker_test.go b/go/pkg/release/publishers/docker_test.go
similarity index 99%
rename from pkg/release/publishers/docker_test.go
rename to go/pkg/release/publishers/docker_test.go
index 81fbf99..8e1c879 100644
--- a/pkg/release/publishers/docker_test.go
+++ b/go/pkg/release/publishers/docker_test.go
@@ -689,12 +689,11 @@ func TestDocker_DockerPublisherBuildBuildxArgsEdgeCasesGood(t *testing.T) {
}
}
}
- if !(foundVersionArg ||
-
- // Note: VERSION is both in BuildArgs and auto-added, so we just check it exists
- foundAutoVersion) {
- t.Fatal("VERSION build arg not found")
- }
+ if !foundVersionArg &&
+ // Note: VERSION is both in BuildArgs and auto-added, so we just check it exists
+ !foundAutoVersion {
+ t.Fatal("VERSION build arg not found")
+ }
if !(foundSimpleArg) {
t.Fatal("SIMPLE_VER build arg not expanded")
}
diff --git a/pkg/release/publishers/github.go b/go/pkg/release/publishers/github.go
similarity index 99%
rename from pkg/release/publishers/github.go
rename to go/pkg/release/publishers/github.go
index edab6bd..555bd5d 100644
--- a/pkg/release/publishers/github.go
+++ b/go/pkg/release/publishers/github.go
@@ -276,7 +276,7 @@ func (p *GitHubPublisher) materializeArtifacts(release *Release) core.Result {
paths = append(paths, localPath)
}
- return core.Ok(githubArtifactMaterialization{paths: paths, cleanup: func() { ax.RemoveAll(tempDir) }})
+ return core.Ok(githubArtifactMaterialization{paths: paths, cleanup: func() { _ = ax.RemoveAll(tempDir) }})
}
func copyArtifactPathToLocal(artifactFS coreio.Medium, sourcePath, destinationPath string) core.Result {
@@ -317,7 +317,7 @@ func copyArtifactFileToLocal(artifactFS coreio.Medium, sourcePath, destinationPa
return core.Fail(core.E("github.copyArtifactFileToLocal", "failed to open artifact", core.NewError(fileResult.Error())))
}
file := fileResult.Value.(core.FsFile)
- defer file.Close()
+ defer func() { _ = file.Close() }()
content, readFailure := stdio.ReadAll(file)
if readFailure != nil {
diff --git a/pkg/release/publishers/github_example_test.go b/go/pkg/release/publishers/github_example_test.go
similarity index 100%
rename from pkg/release/publishers/github_example_test.go
rename to go/pkg/release/publishers/github_example_test.go
diff --git a/pkg/release/publishers/github_test.go b/go/pkg/release/publishers/github_test.go
similarity index 99%
rename from pkg/release/publishers/github_test.go
rename to go/pkg/release/publishers/github_test.go
index 0503195..5d046b8 100644
--- a/pkg/release/publishers/github_test.go
+++ b/go/pkg/release/publishers/github_test.go
@@ -641,9 +641,9 @@ func TestGitHub_ValidateGhCliBad(t *testing.T) {
// the function signature works correctly
err := validateGhCli(context.Background())
if !err.OK {
- if !(core.
+ if !core.Contains(err.Error(), "gh CLI not found") &&
// Either gh is not installed or not authenticated
- Contains(err.Error(), "gh CLI not found") || core.Contains(err.Error(), "not authenticated")) {
+ !core.Contains(err.Error(), "not authenticated") {
t.Fatalf("unexpected error: %s", err.Error())
}
diff --git a/pkg/release/publishers/homebrew.go b/go/pkg/release/publishers/homebrew.go
similarity index 99%
rename from pkg/release/publishers/homebrew.go
rename to go/pkg/release/publishers/homebrew.go
index d7334a8..a77e4ea 100644
--- a/pkg/release/publishers/homebrew.go
+++ b/go/pkg/release/publishers/homebrew.go
@@ -277,7 +277,7 @@ func (p *HomebrewPublisher) commitToTap(ctx context.Context, tap string, data ho
return core.Fail(core.E("homebrew.commitToTap", "failed to create temp directory", core.NewError(tmpDirResult.Error())))
}
tmpDir := tmpDirResult.Value.(string)
- defer func() { ax.RemoveAll(tmpDir) }()
+ defer func() { _ = ax.RemoveAll(tmpDir) }()
// Clone the tap
publisherPrint("Cloning tap %s...", tap)
diff --git a/pkg/release/publishers/homebrew_example_test.go b/go/pkg/release/publishers/homebrew_example_test.go
similarity index 100%
rename from pkg/release/publishers/homebrew_example_test.go
rename to go/pkg/release/publishers/homebrew_example_test.go
diff --git a/pkg/release/publishers/homebrew_test.go b/go/pkg/release/publishers/homebrew_test.go
similarity index 100%
rename from pkg/release/publishers/homebrew_test.go
rename to go/pkg/release/publishers/homebrew_test.go
diff --git a/pkg/release/publishers/integration_test.go b/go/pkg/release/publishers/integration_test.go
similarity index 100%
rename from pkg/release/publishers/integration_test.go
rename to go/pkg/release/publishers/integration_test.go
diff --git a/pkg/release/publishers/linuxkit.go b/go/pkg/release/publishers/linuxkit.go
similarity index 100%
rename from pkg/release/publishers/linuxkit.go
rename to go/pkg/release/publishers/linuxkit.go
diff --git a/pkg/release/publishers/linuxkit_aws.go b/go/pkg/release/publishers/linuxkit_aws.go
similarity index 100%
rename from pkg/release/publishers/linuxkit_aws.go
rename to go/pkg/release/publishers/linuxkit_aws.go
diff --git a/pkg/release/publishers/linuxkit_example_test.go b/go/pkg/release/publishers/linuxkit_example_test.go
similarity index 100%
rename from pkg/release/publishers/linuxkit_example_test.go
rename to go/pkg/release/publishers/linuxkit_example_test.go
diff --git a/pkg/release/publishers/linuxkit_gcp.go b/go/pkg/release/publishers/linuxkit_gcp.go
similarity index 100%
rename from pkg/release/publishers/linuxkit_gcp.go
rename to go/pkg/release/publishers/linuxkit_gcp.go
diff --git a/pkg/release/publishers/linuxkit_iso.go b/go/pkg/release/publishers/linuxkit_iso.go
similarity index 100%
rename from pkg/release/publishers/linuxkit_iso.go
rename to go/pkg/release/publishers/linuxkit_iso.go
diff --git a/pkg/release/publishers/linuxkit_qcow2.go b/go/pkg/release/publishers/linuxkit_qcow2.go
similarity index 100%
rename from pkg/release/publishers/linuxkit_qcow2.go
rename to go/pkg/release/publishers/linuxkit_qcow2.go
diff --git a/pkg/release/publishers/linuxkit_raw.go b/go/pkg/release/publishers/linuxkit_raw.go
similarity index 100%
rename from pkg/release/publishers/linuxkit_raw.go
rename to go/pkg/release/publishers/linuxkit_raw.go
diff --git a/pkg/release/publishers/linuxkit_test.go b/go/pkg/release/publishers/linuxkit_test.go
similarity index 99%
rename from pkg/release/publishers/linuxkit_test.go
rename to go/pkg/release/publishers/linuxkit_test.go
index e6c1737..f5cf4fa 100644
--- a/pkg/release/publishers/linuxkit_test.go
+++ b/go/pkg/release/publishers/linuxkit_test.go
@@ -1319,18 +1319,6 @@ func assertLinuxKitArtifactExists(t *testing.T, result linuxKitPublishFixtureRes
}
}
-func assertLinuxKitPublishError(t *testing.T, format, linuxKitMode, expected string) {
- t.Helper()
-
- result := runLinuxKitPublishFixture(t, []string{format}, linuxKitMode, nil)
- if result.Err.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Err.Error(), expected) {
- t.Fatalf("expected %v to contain %v", result.Err.Error(), expected)
- }
-}
-
func readLinuxKitCloudLog(t *testing.T, path string) string {
t.Helper()
diff --git a/pkg/release/publishers/npm.go b/go/pkg/release/publishers/npm.go
similarity index 99%
rename from pkg/release/publishers/npm.go
rename to go/pkg/release/publishers/npm.go
index 41cc030..396ecb6 100644
--- a/pkg/release/publishers/npm.go
+++ b/go/pkg/release/publishers/npm.go
@@ -207,7 +207,7 @@ func (p *NpmPublisher) executePublish(ctx context.Context, m coreio.Medium, data
return core.Fail(core.E("npm.Publish", "failed to create temp directory", core.NewError(tmpDirResult.Error())))
}
tmpDir := tmpDirResult.Value.(string)
- defer func() { ax.RemoveAll(tmpDir) }()
+ defer func() { _ = ax.RemoveAll(tmpDir) }()
// Create bin directory
binDir := ax.Join(tmpDir, "bin")
diff --git a/pkg/release/publishers/npm_example_test.go b/go/pkg/release/publishers/npm_example_test.go
similarity index 100%
rename from pkg/release/publishers/npm_example_test.go
rename to go/pkg/release/publishers/npm_example_test.go
diff --git a/pkg/release/publishers/npm_test.go b/go/pkg/release/publishers/npm_test.go
similarity index 100%
rename from pkg/release/publishers/npm_test.go
rename to go/pkg/release/publishers/npm_test.go
diff --git a/pkg/release/publishers/output.go b/go/pkg/release/publishers/output.go
similarity index 100%
rename from pkg/release/publishers/output.go
rename to go/pkg/release/publishers/output.go
diff --git a/pkg/release/publishers/publisher.go b/go/pkg/release/publishers/publisher.go
similarity index 100%
rename from pkg/release/publishers/publisher.go
rename to go/pkg/release/publishers/publisher.go
diff --git a/pkg/release/publishers/publisher_example_test.go b/go/pkg/release/publishers/publisher_example_test.go
similarity index 100%
rename from pkg/release/publishers/publisher_example_test.go
rename to go/pkg/release/publishers/publisher_example_test.go
diff --git a/pkg/release/publishers/publisher_test.go b/go/pkg/release/publishers/publisher_test.go
similarity index 100%
rename from pkg/release/publishers/publisher_test.go
rename to go/pkg/release/publishers/publisher_test.go
diff --git a/pkg/release/publishers/scoop.go b/go/pkg/release/publishers/scoop.go
similarity index 99%
rename from pkg/release/publishers/scoop.go
rename to go/pkg/release/publishers/scoop.go
index 643544a..67700e8 100644
--- a/pkg/release/publishers/scoop.go
+++ b/go/pkg/release/publishers/scoop.go
@@ -238,7 +238,7 @@ func (p *ScoopPublisher) commitToBucket(ctx context.Context, bucket string, data
return core.Fail(core.E("scoop.commitToBucket", "failed to create temp directory", core.NewError(tmpDirResult.Error())))
}
tmpDir := tmpDirResult.Value.(string)
- defer func() { ax.RemoveAll(tmpDir) }()
+ defer func() { _ = ax.RemoveAll(tmpDir) }()
publisherPrint("Cloning bucket %s...", bucket)
cloned := publisherRun(ctx, "", nil, "gh", "repo", "clone", bucket, tmpDir, "--", "--depth=1")
diff --git a/pkg/release/publishers/scoop_example_test.go b/go/pkg/release/publishers/scoop_example_test.go
similarity index 100%
rename from pkg/release/publishers/scoop_example_test.go
rename to go/pkg/release/publishers/scoop_example_test.go
diff --git a/pkg/release/publishers/scoop_test.go b/go/pkg/release/publishers/scoop_test.go
similarity index 100%
rename from pkg/release/publishers/scoop_test.go
rename to go/pkg/release/publishers/scoop_test.go
diff --git a/pkg/release/publishers/stdlib_assert_test.go b/go/pkg/release/publishers/stdlib_assert_test.go
similarity index 92%
rename from pkg/release/publishers/stdlib_assert_test.go
rename to go/pkg/release/publishers/stdlib_assert_test.go
index 15409c8..01b832b 100644
--- a/pkg/release/publishers/stdlib_assert_test.go
+++ b/go/pkg/release/publishers/stdlib_assert_test.go
@@ -11,9 +11,7 @@ var (
stdlibAssertEqual = testassert.Equal
stdlibAssertNil = testassert.Nil
stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
)
func requirePublisherOK(t *testing.T, result core.Result) {
diff --git a/pkg/release/publishers/template_funcs.go b/go/pkg/release/publishers/template_funcs.go
similarity index 100%
rename from pkg/release/publishers/template_funcs.go
rename to go/pkg/release/publishers/template_funcs.go
diff --git a/pkg/release/publishers/templates/aur/.SRCINFO.tmpl b/go/pkg/release/publishers/templates/aur/.SRCINFO.tmpl
similarity index 100%
rename from pkg/release/publishers/templates/aur/.SRCINFO.tmpl
rename to go/pkg/release/publishers/templates/aur/.SRCINFO.tmpl
diff --git a/pkg/release/publishers/templates/aur/PKGBUILD.tmpl b/go/pkg/release/publishers/templates/aur/PKGBUILD.tmpl
similarity index 100%
rename from pkg/release/publishers/templates/aur/PKGBUILD.tmpl
rename to go/pkg/release/publishers/templates/aur/PKGBUILD.tmpl
diff --git a/pkg/release/publishers/templates/chocolatey/package.nuspec.tmpl b/go/pkg/release/publishers/templates/chocolatey/package.nuspec.tmpl
similarity index 100%
rename from pkg/release/publishers/templates/chocolatey/package.nuspec.tmpl
rename to go/pkg/release/publishers/templates/chocolatey/package.nuspec.tmpl
diff --git a/pkg/release/publishers/templates/chocolatey/tools/chocolateyinstall.ps1.tmpl b/go/pkg/release/publishers/templates/chocolatey/tools/chocolateyinstall.ps1.tmpl
similarity index 100%
rename from pkg/release/publishers/templates/chocolatey/tools/chocolateyinstall.ps1.tmpl
rename to go/pkg/release/publishers/templates/chocolatey/tools/chocolateyinstall.ps1.tmpl
diff --git a/pkg/release/publishers/templates/homebrew/formula.rb.tmpl b/go/pkg/release/publishers/templates/homebrew/formula.rb.tmpl
similarity index 100%
rename from pkg/release/publishers/templates/homebrew/formula.rb.tmpl
rename to go/pkg/release/publishers/templates/homebrew/formula.rb.tmpl
diff --git a/pkg/release/publishers/templates/npm/install.js.tmpl b/go/pkg/release/publishers/templates/npm/install.js.tmpl
similarity index 100%
rename from pkg/release/publishers/templates/npm/install.js.tmpl
rename to go/pkg/release/publishers/templates/npm/install.js.tmpl
diff --git a/pkg/release/publishers/templates/npm/package.json.tmpl b/go/pkg/release/publishers/templates/npm/package.json.tmpl
similarity index 100%
rename from pkg/release/publishers/templates/npm/package.json.tmpl
rename to go/pkg/release/publishers/templates/npm/package.json.tmpl
diff --git a/pkg/release/publishers/templates/npm/run.js.tmpl b/go/pkg/release/publishers/templates/npm/run.js.tmpl
similarity index 100%
rename from pkg/release/publishers/templates/npm/run.js.tmpl
rename to go/pkg/release/publishers/templates/npm/run.js.tmpl
diff --git a/pkg/release/publishers/templates/scoop/manifest.json.tmpl b/go/pkg/release/publishers/templates/scoop/manifest.json.tmpl
similarity index 100%
rename from pkg/release/publishers/templates/scoop/manifest.json.tmpl
rename to go/pkg/release/publishers/templates/scoop/manifest.json.tmpl
diff --git a/pkg/release/publishers/test_helpers_test.go b/go/pkg/release/publishers/test_helpers_test.go
similarity index 100%
rename from pkg/release/publishers/test_helpers_test.go
rename to go/pkg/release/publishers/test_helpers_test.go
diff --git a/pkg/release/publishers/version_validation_test.go b/go/pkg/release/publishers/version_validation_test.go
similarity index 100%
rename from pkg/release/publishers/version_validation_test.go
rename to go/pkg/release/publishers/version_validation_test.go
diff --git a/pkg/release/release.go b/go/pkg/release/release.go
similarity index 100%
rename from pkg/release/release.go
rename to go/pkg/release/release.go
diff --git a/pkg/release/release_example_test.go b/go/pkg/release/release_example_test.go
similarity index 100%
rename from pkg/release/release_example_test.go
rename to go/pkg/release/release_example_test.go
diff --git a/pkg/release/release_test.go b/go/pkg/release/release_test.go
similarity index 100%
rename from pkg/release/release_test.go
rename to go/pkg/release/release_test.go
diff --git a/pkg/release/sdk.go b/go/pkg/release/sdk.go
similarity index 99%
rename from pkg/release/sdk.go
rename to go/pkg/release/sdk.go
index b292b5e..2b2fa9e 100644
--- a/pkg/release/sdk.go
+++ b/go/pkg/release/sdk.go
@@ -201,7 +201,7 @@ func materializeTaggedSDKSpec(ctx context.Context, projectDir, tag, specPath str
return core.Fail(core.E("release.materializeTaggedSDKSpec", "failed to write tagged spec", core.NewError(written.Error())))
}
- return core.Ok(taggedSDKSpec{path: tempPath, cleanup: func() { ax.RemoveAll(tempDir) }})
+ return core.Ok(taggedSDKSpec{path: tempPath, cleanup: func() { _ = ax.RemoveAll(tempDir) }})
}
func resolveReleaseSDKConfig(projectDir string, cfg *Config) core.Result {
diff --git a/pkg/release/sdk_example_test.go b/go/pkg/release/sdk_example_test.go
similarity index 100%
rename from pkg/release/sdk_example_test.go
rename to go/pkg/release/sdk_example_test.go
diff --git a/pkg/release/sdk_test.go b/go/pkg/release/sdk_test.go
similarity index 100%
rename from pkg/release/sdk_test.go
rename to go/pkg/release/sdk_test.go
diff --git a/pkg/release/stdlib_assert_test.go b/go/pkg/release/stdlib_assert_test.go
similarity index 87%
rename from pkg/release/stdlib_assert_test.go
rename to go/pkg/release/stdlib_assert_test.go
index a4c80f1..46044e6 100644
--- a/pkg/release/stdlib_assert_test.go
+++ b/go/pkg/release/stdlib_assert_test.go
@@ -6,7 +6,6 @@ var (
stdlibAssertEqual = testassert.Equal
stdlibAssertNil = testassert.Nil
stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
stdlibAssertContains = testassert.Contains
stdlibAssertElementsMatch = testassert.ElementsMatch
)
diff --git a/pkg/release/test_helpers_test.go b/go/pkg/release/test_helpers_test.go
similarity index 100%
rename from pkg/release/test_helpers_test.go
rename to go/pkg/release/test_helpers_test.go
diff --git a/pkg/release/version.go b/go/pkg/release/version.go
similarity index 100%
rename from pkg/release/version.go
rename to go/pkg/release/version.go
diff --git a/pkg/release/version_example_test.go b/go/pkg/release/version_example_test.go
similarity index 100%
rename from pkg/release/version_example_test.go
rename to go/pkg/release/version_example_test.go
diff --git a/pkg/release/version_test.go b/go/pkg/release/version_test.go
similarity index 100%
rename from pkg/release/version_test.go
rename to go/pkg/release/version_test.go
diff --git a/pkg/sdk/breaking_test.go b/go/pkg/sdk/breaking_test.go
similarity index 100%
rename from pkg/sdk/breaking_test.go
rename to go/pkg/sdk/breaking_test.go
diff --git a/pkg/sdk/detect.go b/go/pkg/sdk/detect.go
similarity index 100%
rename from pkg/sdk/detect.go
rename to go/pkg/sdk/detect.go
diff --git a/pkg/sdk/detect_example_test.go b/go/pkg/sdk/detect_example_test.go
similarity index 100%
rename from pkg/sdk/detect_example_test.go
rename to go/pkg/sdk/detect_example_test.go
diff --git a/pkg/sdk/detect_test.go b/go/pkg/sdk/detect_test.go
similarity index 100%
rename from pkg/sdk/detect_test.go
rename to go/pkg/sdk/detect_test.go
diff --git a/pkg/sdk/diff.go b/go/pkg/sdk/diff.go
similarity index 100%
rename from pkg/sdk/diff.go
rename to go/pkg/sdk/diff.go
diff --git a/pkg/sdk/diff_example_test.go b/go/pkg/sdk/diff_example_test.go
similarity index 100%
rename from pkg/sdk/diff_example_test.go
rename to go/pkg/sdk/diff_example_test.go
diff --git a/pkg/sdk/diff_test.go b/go/pkg/sdk/diff_test.go
similarity index 100%
rename from pkg/sdk/diff_test.go
rename to go/pkg/sdk/diff_test.go
diff --git a/pkg/sdk/generation_test.go b/go/pkg/sdk/generation_test.go
similarity index 100%
rename from pkg/sdk/generation_test.go
rename to go/pkg/sdk/generation_test.go
diff --git a/pkg/sdk/generators/docker_runtime.go b/go/pkg/sdk/generators/docker_runtime.go
similarity index 100%
rename from pkg/sdk/generators/docker_runtime.go
rename to go/pkg/sdk/generators/docker_runtime.go
diff --git a/pkg/sdk/generators/docker_runtime_test.go b/go/pkg/sdk/generators/docker_runtime_test.go
similarity index 100%
rename from pkg/sdk/generators/docker_runtime_test.go
rename to go/pkg/sdk/generators/docker_runtime_test.go
diff --git a/pkg/sdk/generators/generator.go b/go/pkg/sdk/generators/generator.go
similarity index 100%
rename from pkg/sdk/generators/generator.go
rename to go/pkg/sdk/generators/generator.go
diff --git a/pkg/sdk/generators/generator_example_test.go b/go/pkg/sdk/generators/generator_example_test.go
similarity index 100%
rename from pkg/sdk/generators/generator_example_test.go
rename to go/pkg/sdk/generators/generator_example_test.go
diff --git a/pkg/sdk/generators/generator_test.go b/go/pkg/sdk/generators/generator_test.go
similarity index 100%
rename from pkg/sdk/generators/generator_test.go
rename to go/pkg/sdk/generators/generator_test.go
diff --git a/pkg/sdk/generators/go.go b/go/pkg/sdk/generators/go.go
similarity index 100%
rename from pkg/sdk/generators/go.go
rename to go/pkg/sdk/generators/go.go
diff --git a/pkg/sdk/generators/go_example_test.go b/go/pkg/sdk/generators/go_example_test.go
similarity index 100%
rename from pkg/sdk/generators/go_example_test.go
rename to go/pkg/sdk/generators/go_example_test.go
diff --git a/pkg/sdk/generators/go_test.go b/go/pkg/sdk/generators/go_test.go
similarity index 100%
rename from pkg/sdk/generators/go_test.go
rename to go/pkg/sdk/generators/go_test.go
diff --git a/pkg/sdk/generators/php.go b/go/pkg/sdk/generators/php.go
similarity index 100%
rename from pkg/sdk/generators/php.go
rename to go/pkg/sdk/generators/php.go
diff --git a/pkg/sdk/generators/php_example_test.go b/go/pkg/sdk/generators/php_example_test.go
similarity index 100%
rename from pkg/sdk/generators/php_example_test.go
rename to go/pkg/sdk/generators/php_example_test.go
diff --git a/pkg/sdk/generators/php_test.go b/go/pkg/sdk/generators/php_test.go
similarity index 100%
rename from pkg/sdk/generators/php_test.go
rename to go/pkg/sdk/generators/php_test.go
diff --git a/pkg/sdk/generators/python.go b/go/pkg/sdk/generators/python.go
similarity index 100%
rename from pkg/sdk/generators/python.go
rename to go/pkg/sdk/generators/python.go
diff --git a/pkg/sdk/generators/python_example_test.go b/go/pkg/sdk/generators/python_example_test.go
similarity index 100%
rename from pkg/sdk/generators/python_example_test.go
rename to go/pkg/sdk/generators/python_example_test.go
diff --git a/pkg/sdk/generators/python_test.go b/go/pkg/sdk/generators/python_test.go
similarity index 100%
rename from pkg/sdk/generators/python_test.go
rename to go/pkg/sdk/generators/python_test.go
diff --git a/go/pkg/sdk/generators/stdlib_assert_test.go b/go/pkg/sdk/generators/stdlib_assert_test.go
new file mode 100644
index 0000000..f1e4358
--- /dev/null
+++ b/go/pkg/sdk/generators/stdlib_assert_test.go
@@ -0,0 +1,8 @@
+package generators
+
+import "dappco.re/go/build/internal/testassert"
+
+var (
+ stdlibAssertEqual = testassert.Equal
+ stdlibAssertContains = testassert.Contains
+)
diff --git a/pkg/sdk/generators/typescript.go b/go/pkg/sdk/generators/typescript.go
similarity index 100%
rename from pkg/sdk/generators/typescript.go
rename to go/pkg/sdk/generators/typescript.go
diff --git a/pkg/sdk/generators/typescript_example_test.go b/go/pkg/sdk/generators/typescript_example_test.go
similarity index 100%
rename from pkg/sdk/generators/typescript_example_test.go
rename to go/pkg/sdk/generators/typescript_example_test.go
diff --git a/pkg/sdk/generators/typescript_test.go b/go/pkg/sdk/generators/typescript_test.go
similarity index 100%
rename from pkg/sdk/generators/typescript_test.go
rename to go/pkg/sdk/generators/typescript_test.go
diff --git a/pkg/sdk/sdk.go b/go/pkg/sdk/sdk.go
similarity index 100%
rename from pkg/sdk/sdk.go
rename to go/pkg/sdk/sdk.go
diff --git a/pkg/sdk/sdk_example_test.go b/go/pkg/sdk/sdk_example_test.go
similarity index 100%
rename from pkg/sdk/sdk_example_test.go
rename to go/pkg/sdk/sdk_example_test.go
diff --git a/pkg/sdk/sdk_test.go b/go/pkg/sdk/sdk_test.go
similarity index 100%
rename from pkg/sdk/sdk_test.go
rename to go/pkg/sdk/sdk_test.go
diff --git a/pkg/sdk/stdlib_assert_test.go b/go/pkg/sdk/stdlib_assert_test.go
similarity index 72%
rename from pkg/sdk/stdlib_assert_test.go
rename to go/pkg/sdk/stdlib_assert_test.go
index a6b6330..b6e4520 100644
--- a/pkg/sdk/stdlib_assert_test.go
+++ b/go/pkg/sdk/stdlib_assert_test.go
@@ -6,7 +6,5 @@ var (
stdlibAssertEqual = testassert.Equal
stdlibAssertNil = testassert.Nil
stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
)
diff --git a/pkg/sdk/validate.go b/go/pkg/sdk/validate.go
similarity index 100%
rename from pkg/sdk/validate.go
rename to go/pkg/sdk/validate.go
diff --git a/pkg/sdk/validate_example_test.go b/go/pkg/sdk/validate_example_test.go
similarity index 100%
rename from pkg/sdk/validate_example_test.go
rename to go/pkg/sdk/validate_example_test.go
diff --git a/pkg/sdk/validate_test.go b/go/pkg/sdk/validate_test.go
similarity index 100%
rename from pkg/sdk/validate_test.go
rename to go/pkg/sdk/validate_test.go
diff --git a/pkg/service/agentic.go b/go/pkg/service/agentic.go
similarity index 100%
rename from pkg/service/agentic.go
rename to go/pkg/service/agentic.go
diff --git a/pkg/service/agentic_example_test.go b/go/pkg/service/agentic_example_test.go
similarity index 100%
rename from pkg/service/agentic_example_test.go
rename to go/pkg/service/agentic_example_test.go
diff --git a/pkg/service/agentic_test.go b/go/pkg/service/agentic_test.go
similarity index 100%
rename from pkg/service/agentic_test.go
rename to go/pkg/service/agentic_test.go
diff --git a/pkg/service/config.go b/go/pkg/service/config.go
similarity index 100%
rename from pkg/service/config.go
rename to go/pkg/service/config.go
diff --git a/pkg/service/config_example_test.go b/go/pkg/service/config_example_test.go
similarity index 100%
rename from pkg/service/config_example_test.go
rename to go/pkg/service/config_example_test.go
diff --git a/pkg/service/config_test.go b/go/pkg/service/config_test.go
similarity index 100%
rename from pkg/service/config_test.go
rename to go/pkg/service/config_test.go
diff --git a/pkg/service/daemon.go b/go/pkg/service/daemon.go
similarity index 97%
rename from pkg/service/daemon.go
rename to go/pkg/service/daemon.go
index c98a5ad..dee8d01 100644
--- a/pkg/service/daemon.go
+++ b/go/pkg/service/daemon.go
@@ -8,7 +8,6 @@ import (
core "dappco.re/go"
"dappco.re/go/build/internal/ax"
- buildapi "dappco.re/go/build/pkg/api"
coreapi "dappco.re/go/build/pkg/api"
providerpkg "dappco.re/go/build/pkg/api/provider"
"dappco.re/go/build/pkg/build"
@@ -29,11 +28,11 @@ type processDaemon interface {
SetReady(ready bool)
}
-var (
- newHub = events.NewHub
- newBuildProvider = func(projectDir string, hub *events.Hub) providerpkg.Provider {
- return buildapi.NewProvider(projectDir, hub)
- }
+ var (
+ newHub = events.NewHub
+ newBuildProvider = func(projectDir string, hub *events.Hub) providerpkg.Provider {
+ return coreapi.NewProvider(projectDir, hub)
+ }
newProviderRegistry = providerpkg.NewRegistry
newAPIEngine = func(opts ...coreapi.Option) core.Result { return coreapi.New(opts...) }
newMCPServer = defaultNewMCPServer
diff --git a/pkg/service/daemon_example_test.go b/go/pkg/service/daemon_example_test.go
similarity index 100%
rename from pkg/service/daemon_example_test.go
rename to go/pkg/service/daemon_example_test.go
diff --git a/pkg/service/daemon_run_test.go b/go/pkg/service/daemon_run_test.go
similarity index 100%
rename from pkg/service/daemon_run_test.go
rename to go/pkg/service/daemon_run_test.go
diff --git a/pkg/service/daemon_test.go b/go/pkg/service/daemon_test.go
similarity index 100%
rename from pkg/service/daemon_test.go
rename to go/pkg/service/daemon_test.go
diff --git a/pkg/service/export.go b/go/pkg/service/export.go
similarity index 100%
rename from pkg/service/export.go
rename to go/pkg/service/export.go
diff --git a/pkg/service/export_example_test.go b/go/pkg/service/export_example_test.go
similarity index 100%
rename from pkg/service/export_example_test.go
rename to go/pkg/service/export_example_test.go
diff --git a/pkg/service/export_test.go b/go/pkg/service/export_test.go
similarity index 100%
rename from pkg/service/export_test.go
rename to go/pkg/service/export_test.go
diff --git a/pkg/service/manager.go b/go/pkg/service/manager.go
similarity index 100%
rename from pkg/service/manager.go
rename to go/pkg/service/manager.go
diff --git a/pkg/service/manager_example_test.go b/go/pkg/service/manager_example_test.go
similarity index 100%
rename from pkg/service/manager_example_test.go
rename to go/pkg/service/manager_example_test.go
diff --git a/pkg/service/manager_test.go b/go/pkg/service/manager_test.go
similarity index 100%
rename from pkg/service/manager_test.go
rename to go/pkg/service/manager_test.go
diff --git a/pkg/service/mcp.go b/go/pkg/service/mcp.go
similarity index 100%
rename from pkg/service/mcp.go
rename to go/pkg/service/mcp.go
diff --git a/pkg/service/mcp_test.go b/go/pkg/service/mcp_test.go
similarity index 98%
rename from pkg/service/mcp_test.go
rename to go/pkg/service/mcp_test.go
index ea97143..4a15e02 100644
--- a/pkg/service/mcp_test.go
+++ b/go/pkg/service/mcp_test.go
@@ -100,7 +100,7 @@ func postTool(t *testing.T, url string) string {
t.Fatalf("unexpected error: %v", err)
}
- defer response.Body.Close()
+ defer func() { _ = response.Body.Close() }()
body, err := io.ReadAll(response.Body)
if err != nil {
diff --git a/pkg/service/process_daemon.go b/go/pkg/service/process_daemon.go
similarity index 100%
rename from pkg/service/process_daemon.go
rename to go/pkg/service/process_daemon.go
diff --git a/pkg/service/stdlib_assert_test.go b/go/pkg/service/stdlib_assert_test.go
similarity index 92%
rename from pkg/service/stdlib_assert_test.go
rename to go/pkg/service/stdlib_assert_test.go
index 3bb0514..6b909cd 100644
--- a/pkg/service/stdlib_assert_test.go
+++ b/go/pkg/service/stdlib_assert_test.go
@@ -10,10 +10,7 @@ import (
var (
stdlibAssertEqual = testassert.Equal
stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
)
type serviceFatal interface {
diff --git a/pkg/service/test_helpers_test.go b/go/pkg/service/test_helpers_test.go
similarity index 100%
rename from pkg/service/test_helpers_test.go
rename to go/pkg/service/test_helpers_test.go
diff --git a/pkg/storage/storage.go b/go/pkg/storage/storage.go
similarity index 100%
rename from pkg/storage/storage.go
rename to go/pkg/storage/storage.go
diff --git a/pkg/storage/storage_example_test.go b/go/pkg/storage/storage_example_test.go
similarity index 100%
rename from pkg/storage/storage_example_test.go
rename to go/pkg/storage/storage_example_test.go
diff --git a/pkg/storage/storage_test.go b/go/pkg/storage/storage_test.go
similarity index 100%
rename from pkg/storage/storage_test.go
rename to go/pkg/storage/storage_test.go
diff --git a/internal/projectdetect/stdlib_assert_test.go b/internal/projectdetect/stdlib_assert_test.go
deleted file mode 100644
index 75055d4..0000000
--- a/internal/projectdetect/stdlib_assert_test.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package projectdetect
-
-import "dappco.re/go/build/internal/testassert"
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
diff --git a/internal/sdkcfg/stdlib_assert_test.go b/internal/sdkcfg/stdlib_assert_test.go
deleted file mode 100644
index 37d02a6..0000000
--- a/internal/sdkcfg/stdlib_assert_test.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package sdkcfg
-
-import "dappco.re/go/build/internal/testassert"
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
diff --git a/pkg/api/ui/dist/core-build.js b/pkg/api/ui/dist/core-build.js
deleted file mode 100644
index 22cf1e7..0000000
--- a/pkg/api/ui/dist/core-build.js
+++ /dev/null
@@ -1,2496 +0,0 @@
-/**
- * @license
- * Copyright 2019 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-const X = globalThis, ne = X.ShadowRoot && (X.ShadyCSS === void 0 || X.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, oe = Symbol(), ce = /* @__PURE__ */ new WeakMap();
-let _e = class {
- constructor(e, s, a) {
- if (this._$cssResult$ = !0, a !== oe) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
- this.cssText = e, this.t = s;
- }
- get styleSheet() {
- let e = this.o;
- const s = this.t;
- if (ne && e === void 0) {
- const a = s !== void 0 && s.length === 1;
- a && (e = ce.get(s)), e === void 0 && ((this.o = e = new CSSStyleSheet()).replaceSync(this.cssText), a && ce.set(s, e));
- }
- return e;
- }
- toString() {
- return this.cssText;
- }
-};
-const Ee = (t) => new _e(typeof t == "string" ? t : t + "", void 0, oe), K = (t, ...e) => {
- const s = t.length === 1 ? t[0] : e.reduce((a, i, r) => a + ((n) => {
- if (n._$cssResult$ === !0) return n.cssText;
- if (typeof n == "number") return n;
- throw Error("Value passed to 'css' function must be a 'css' function result: " + n + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.");
- })(i) + t[r + 1], t[0]);
- return new _e(s, t, oe);
-}, Oe = (t, e) => {
- if (ne) t.adoptedStyleSheets = e.map((s) => s instanceof CSSStyleSheet ? s : s.styleSheet);
- else for (const s of e) {
- const a = document.createElement("style"), i = X.litNonce;
- i !== void 0 && a.setAttribute("nonce", i), a.textContent = s.cssText, t.appendChild(a);
- }
-}, pe = ne ? (t) => t : (t) => t instanceof CSSStyleSheet ? ((e) => {
- let s = "";
- for (const a of e.cssRules) s += a.cssText;
- return Ee(s);
-})(t) : t;
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-const { is: De, defineProperty: Re, getOwnPropertyDescriptor: Ue, getOwnPropertyNames: Te, getOwnPropertySymbols: ze, getPrototypeOf: je } = Object, C = globalThis, fe = C.trustedTypes, Be = fe ? fe.emptyScript : "", se = C.reactiveElementPolyfillSupport, q = (t, e) => t, Q = { toAttribute(t, e) {
- switch (e) {
- case Boolean:
- t = t ? Be : null;
- break;
- case Object:
- case Array:
- t = t == null ? t : JSON.stringify(t);
- }
- return t;
-}, fromAttribute(t, e) {
- let s = t;
- switch (e) {
- case Boolean:
- s = t !== null;
- break;
- case Number:
- s = t === null ? null : Number(t);
- break;
- case Object:
- case Array:
- try {
- s = JSON.parse(t);
- } catch {
- s = null;
- }
- }
- return s;
-} }, le = (t, e) => !De(t, e), he = { attribute: !0, type: String, converter: Q, reflect: !1, useDefault: !1, hasChanged: le };
-Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), C.litPropertyMetadata ?? (C.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
-let j = class extends HTMLElement {
- static addInitializer(e) {
- this._$Ei(), (this.l ?? (this.l = [])).push(e);
- }
- static get observedAttributes() {
- return this.finalize(), this._$Eh && [...this._$Eh.keys()];
- }
- static createProperty(e, s = he) {
- if (s.state && (s.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(e) && ((s = Object.create(s)).wrapped = !0), this.elementProperties.set(e, s), !s.noAccessor) {
- const a = Symbol(), i = this.getPropertyDescriptor(e, a, s);
- i !== void 0 && Re(this.prototype, e, i);
- }
- }
- static getPropertyDescriptor(e, s, a) {
- const { get: i, set: r } = Ue(this.prototype, e) ?? { get() {
- return this[s];
- }, set(n) {
- this[s] = n;
- } };
- return { get: i, set(n) {
- const c = i == null ? void 0 : i.call(this);
- r == null || r.call(this, n), this.requestUpdate(e, c, a);
- }, configurable: !0, enumerable: !0 };
- }
- static getPropertyOptions(e) {
- return this.elementProperties.get(e) ?? he;
- }
- static _$Ei() {
- if (this.hasOwnProperty(q("elementProperties"))) return;
- const e = je(this);
- e.finalize(), e.l !== void 0 && (this.l = [...e.l]), this.elementProperties = new Map(e.elementProperties);
- }
- static finalize() {
- if (this.hasOwnProperty(q("finalized"))) return;
- if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(q("properties"))) {
- const s = this.properties, a = [...Te(s), ...ze(s)];
- for (const i of a) this.createProperty(i, s[i]);
- }
- const e = this[Symbol.metadata];
- if (e !== null) {
- const s = litPropertyMetadata.get(e);
- if (s !== void 0) for (const [a, i] of s) this.elementProperties.set(a, i);
- }
- this._$Eh = /* @__PURE__ */ new Map();
- for (const [s, a] of this.elementProperties) {
- const i = this._$Eu(s, a);
- i !== void 0 && this._$Eh.set(i, s);
- }
- this.elementStyles = this.finalizeStyles(this.styles);
- }
- static finalizeStyles(e) {
- const s = [];
- if (Array.isArray(e)) {
- const a = new Set(e.flat(1 / 0).reverse());
- for (const i of a) s.unshift(pe(i));
- } else e !== void 0 && s.push(pe(e));
- return s;
- }
- static _$Eu(e, s) {
- const a = s.attribute;
- return a === !1 ? void 0 : typeof a == "string" ? a : typeof e == "string" ? e.toLowerCase() : void 0;
- }
- constructor() {
- super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev();
- }
- _$Ev() {
- var e;
- this._$ES = new Promise((s) => this.enableUpdating = s), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (e = this.constructor.l) == null || e.forEach((s) => s(this));
- }
- addController(e) {
- var s;
- (this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(e), this.renderRoot !== void 0 && this.isConnected && ((s = e.hostConnected) == null || s.call(e));
- }
- removeController(e) {
- var s;
- (s = this._$EO) == null || s.delete(e);
- }
- _$E_() {
- const e = /* @__PURE__ */ new Map(), s = this.constructor.elementProperties;
- for (const a of s.keys()) this.hasOwnProperty(a) && (e.set(a, this[a]), delete this[a]);
- e.size > 0 && (this._$Ep = e);
- }
- createRenderRoot() {
- const e = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
- return Oe(e, this.constructor.elementStyles), e;
- }
- connectedCallback() {
- var e;
- this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(!0), (e = this._$EO) == null || e.forEach((s) => {
- var a;
- return (a = s.hostConnected) == null ? void 0 : a.call(s);
- });
- }
- enableUpdating(e) {
- }
- disconnectedCallback() {
- var e;
- (e = this._$EO) == null || e.forEach((s) => {
- var a;
- return (a = s.hostDisconnected) == null ? void 0 : a.call(s);
- });
- }
- attributeChangedCallback(e, s, a) {
- this._$AK(e, a);
- }
- _$ET(e, s) {
- var r;
- const a = this.constructor.elementProperties.get(e), i = this.constructor._$Eu(e, a);
- if (i !== void 0 && a.reflect === !0) {
- const n = (((r = a.converter) == null ? void 0 : r.toAttribute) !== void 0 ? a.converter : Q).toAttribute(s, a.type);
- this._$Em = e, n == null ? this.removeAttribute(i) : this.setAttribute(i, n), this._$Em = null;
- }
- }
- _$AK(e, s) {
- var r, n;
- const a = this.constructor, i = a._$Eh.get(e);
- if (i !== void 0 && this._$Em !== i) {
- const c = a.getPropertyOptions(i), d = typeof c.converter == "function" ? { fromAttribute: c.converter } : ((r = c.converter) == null ? void 0 : r.fromAttribute) !== void 0 ? c.converter : Q;
- this._$Em = i;
- const h = d.fromAttribute(s, c.type);
- this[i] = h ?? ((n = this._$Ej) == null ? void 0 : n.get(i)) ?? h, this._$Em = null;
- }
- }
- requestUpdate(e, s, a, i = !1, r) {
- var n;
- if (e !== void 0) {
- const c = this.constructor;
- if (i === !1 && (r = this[e]), a ?? (a = c.getPropertyOptions(e)), !((a.hasChanged ?? le)(r, s) || a.useDefault && a.reflect && r === ((n = this._$Ej) == null ? void 0 : n.get(e)) && !this.hasAttribute(c._$Eu(e, a)))) return;
- this.C(e, s, a);
- }
- this.isUpdatePending === !1 && (this._$ES = this._$EP());
- }
- C(e, s, { useDefault: a, reflect: i, wrapped: r }, n) {
- a && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(e) && (this._$Ej.set(e, n ?? s ?? this[e]), r !== !0 || n !== void 0) || (this._$AL.has(e) || (this.hasUpdated || a || (s = void 0), this._$AL.set(e, s)), i === !0 && this._$Em !== e && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(e));
- }
- async _$EP() {
- this.isUpdatePending = !0;
- try {
- await this._$ES;
- } catch (s) {
- Promise.reject(s);
- }
- const e = this.scheduleUpdate();
- return e != null && await e, !this.isUpdatePending;
- }
- scheduleUpdate() {
- return this.performUpdate();
- }
- performUpdate() {
- var a;
- if (!this.isUpdatePending) return;
- if (!this.hasUpdated) {
- if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
- for (const [r, n] of this._$Ep) this[r] = n;
- this._$Ep = void 0;
- }
- const i = this.constructor.elementProperties;
- if (i.size > 0) for (const [r, n] of i) {
- const { wrapped: c } = n, d = this[r];
- c !== !0 || this._$AL.has(r) || d === void 0 || this.C(r, void 0, n, d);
- }
- }
- let e = !1;
- const s = this._$AL;
- try {
- e = this.shouldUpdate(s), e ? (this.willUpdate(s), (a = this._$EO) == null || a.forEach((i) => {
- var r;
- return (r = i.hostUpdate) == null ? void 0 : r.call(i);
- }), this.update(s)) : this._$EM();
- } catch (i) {
- throw e = !1, this._$EM(), i;
- }
- e && this._$AE(s);
- }
- willUpdate(e) {
- }
- _$AE(e) {
- var s;
- (s = this._$EO) == null || s.forEach((a) => {
- var i;
- return (i = a.hostUpdated) == null ? void 0 : i.call(a);
- }), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(e)), this.updated(e);
- }
- _$EM() {
- this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1;
- }
- get updateComplete() {
- return this.getUpdateComplete();
- }
- getUpdateComplete() {
- return this._$ES;
- }
- shouldUpdate(e) {
- return !0;
- }
- update(e) {
- this._$Eq && (this._$Eq = this._$Eq.forEach((s) => this._$ET(s, this[s]))), this._$EM();
- }
- updated(e) {
- }
- firstUpdated(e) {
- }
-};
-j.elementStyles = [], j.shadowRootOptions = { mode: "open" }, j[q("elementProperties")] = /* @__PURE__ */ new Map(), j[q("finalized")] = /* @__PURE__ */ new Map(), se == null || se({ ReactiveElement: j }), (C.reactiveElementVersions ?? (C.reactiveElementVersions = [])).push("2.1.2");
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-const I = globalThis, ue = (t) => t, Y = I.trustedTypes, ge = Y ? Y.createPolicy("lit-html", { createHTML: (t) => t }) : void 0, ke = "$lit$", P = `lit$${Math.random().toFixed(9).slice(2)}$`, xe = "?" + P, Ne = `<${xe}>`, T = document, F = () => T.createComment(""), G = (t) => t === null || typeof t != "object" && typeof t != "function", de = Array.isArray, Me = (t) => de(t) || typeof (t == null ? void 0 : t[Symbol.iterator]) == "function", ie = `[
-\f\r]`, L = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, be = /-->/g, me = />/g, D = RegExp(`>|${ie}(?:([^\\s"'>=/]+)(${ie}*=${ie}*(?:[^
-\f\r"'\`<>=]|("|')|))|$)`, "g"), ve = /'/g, $e = /"/g, Ae = /^(?:script|style|textarea|title)$/i, He = (t) => (e, ...s) => ({ _$litType$: t, strings: e, values: s }), o = He(1), B = Symbol.for("lit-noChange"), l = Symbol.for("lit-nothing"), ye = /* @__PURE__ */ new WeakMap(), R = T.createTreeWalker(T, 129);
-function Se(t, e) {
- if (!de(t) || !t.hasOwnProperty("raw")) throw Error("invalid template strings array");
- return ge !== void 0 ? ge.createHTML(e) : e;
-}
-const We = (t, e) => {
- const s = t.length - 1, a = [];
- let i, r = e === 2 ? "" : e === 3 ? "" : "")), a];
-};
-class V {
- constructor({ strings: e, _$litType$: s }, a) {
- let i;
- this.parts = [];
- let r = 0, n = 0;
- const c = e.length - 1, d = this.parts, [h, u] = We(e, s);
- if (this.el = V.createElement(h, a), R.currentNode = this.el.content, s === 2 || s === 3) {
- const f = this.el.content.firstChild;
- f.replaceWith(...f.childNodes);
- }
- for (; (i = R.nextNode()) !== null && d.length < c; ) {
- if (i.nodeType === 1) {
- if (i.hasAttributes()) for (const f of i.getAttributeNames()) if (f.endsWith(ke)) {
- const b = u[n++], v = i.getAttribute(f).split(P), w = /([.?@])?(.*)/.exec(b);
- d.push({ type: 1, index: r, name: w[2], strings: v, ctor: w[1] === "." ? qe : w[1] === "?" ? Ie : w[1] === "@" ? Fe : ee }), i.removeAttribute(f);
- } else f.startsWith(P) && (d.push({ type: 6, index: r }), i.removeAttribute(f));
- if (Ae.test(i.tagName)) {
- const f = i.textContent.split(P), b = f.length - 1;
- if (b > 0) {
- i.textContent = Y ? Y.emptyScript : "";
- for (let v = 0; v < b; v++) i.append(f[v], F()), R.nextNode(), d.push({ type: 2, index: ++r });
- i.append(f[b], F());
- }
- }
- } else if (i.nodeType === 8) if (i.data === xe) d.push({ type: 2, index: r });
- else {
- let f = -1;
- for (; (f = i.data.indexOf(P, f + 1)) !== -1; ) d.push({ type: 7, index: r }), f += P.length - 1;
- }
- r++;
- }
- }
- static createElement(e, s) {
- const a = T.createElement("template");
- return a.innerHTML = e, a;
- }
-}
-function N(t, e, s = t, a) {
- var n, c;
- if (e === B) return e;
- let i = a !== void 0 ? (n = s._$Co) == null ? void 0 : n[a] : s._$Cl;
- const r = G(e) ? void 0 : e._$litDirective$;
- return (i == null ? void 0 : i.constructor) !== r && ((c = i == null ? void 0 : i._$AO) == null || c.call(i, !1), r === void 0 ? i = void 0 : (i = new r(t), i._$AT(t, s, a)), a !== void 0 ? (s._$Co ?? (s._$Co = []))[a] = i : s._$Cl = i), i !== void 0 && (e = N(t, i._$AS(t, e.values), i, a)), e;
-}
-class Le {
- constructor(e, s) {
- this._$AV = [], this._$AN = void 0, this._$AD = e, this._$AM = s;
- }
- get parentNode() {
- return this._$AM.parentNode;
- }
- get _$AU() {
- return this._$AM._$AU;
- }
- u(e) {
- const { el: { content: s }, parts: a } = this._$AD, i = ((e == null ? void 0 : e.creationScope) ?? T).importNode(s, !0);
- R.currentNode = i;
- let r = R.nextNode(), n = 0, c = 0, d = a[0];
- for (; d !== void 0; ) {
- if (n === d.index) {
- let h;
- d.type === 2 ? h = new J(r, r.nextSibling, this, e) : d.type === 1 ? h = new d.ctor(r, d.name, d.strings, this, e) : d.type === 6 && (h = new Ge(r, this, e)), this._$AV.push(h), d = a[++c];
- }
- n !== (d == null ? void 0 : d.index) && (r = R.nextNode(), n++);
- }
- return R.currentNode = T, i;
- }
- p(e) {
- let s = 0;
- for (const a of this._$AV) a !== void 0 && (a.strings !== void 0 ? (a._$AI(e, a, s), s += a.strings.length - 2) : a._$AI(e[s])), s++;
- }
-}
-class J {
- get _$AU() {
- var e;
- return ((e = this._$AM) == null ? void 0 : e._$AU) ?? this._$Cv;
- }
- constructor(e, s, a, i) {
- this.type = 2, this._$AH = l, this._$AN = void 0, this._$AA = e, this._$AB = s, this._$AM = a, this.options = i, this._$Cv = (i == null ? void 0 : i.isConnected) ?? !0;
- }
- get parentNode() {
- let e = this._$AA.parentNode;
- const s = this._$AM;
- return s !== void 0 && (e == null ? void 0 : e.nodeType) === 11 && (e = s.parentNode), e;
- }
- get startNode() {
- return this._$AA;
- }
- get endNode() {
- return this._$AB;
- }
- _$AI(e, s = this) {
- e = N(this, e, s), G(e) ? e === l || e == null || e === "" ? (this._$AH !== l && this._$AR(), this._$AH = l) : e !== this._$AH && e !== B && this._(e) : e._$litType$ !== void 0 ? this.$(e) : e.nodeType !== void 0 ? this.T(e) : Me(e) ? this.k(e) : this._(e);
- }
- O(e) {
- return this._$AA.parentNode.insertBefore(e, this._$AB);
- }
- T(e) {
- this._$AH !== e && (this._$AR(), this._$AH = this.O(e));
- }
- _(e) {
- this._$AH !== l && G(this._$AH) ? this._$AA.nextSibling.data = e : this.T(T.createTextNode(e)), this._$AH = e;
- }
- $(e) {
- var r;
- const { values: s, _$litType$: a } = e, i = typeof a == "number" ? this._$AC(e) : (a.el === void 0 && (a.el = V.createElement(Se(a.h, a.h[0]), this.options)), a);
- if (((r = this._$AH) == null ? void 0 : r._$AD) === i) this._$AH.p(s);
- else {
- const n = new Le(i, this), c = n.u(this.options);
- n.p(s), this.T(c), this._$AH = n;
- }
- }
- _$AC(e) {
- let s = ye.get(e.strings);
- return s === void 0 && ye.set(e.strings, s = new V(e)), s;
- }
- k(e) {
- de(this._$AH) || (this._$AH = [], this._$AR());
- const s = this._$AH;
- let a, i = 0;
- for (const r of e) i === s.length ? s.push(a = new J(this.O(F()), this.O(F()), this, this.options)) : a = s[i], a._$AI(r), i++;
- i < s.length && (this._$AR(a && a._$AB.nextSibling, i), s.length = i);
- }
- _$AR(e = this._$AA.nextSibling, s) {
- var a;
- for ((a = this._$AP) == null ? void 0 : a.call(this, !1, !0, s); e !== this._$AB; ) {
- const i = ue(e).nextSibling;
- ue(e).remove(), e = i;
- }
- }
- setConnected(e) {
- var s;
- this._$AM === void 0 && (this._$Cv = e, (s = this._$AP) == null || s.call(this, e));
- }
-}
-class ee {
- get tagName() {
- return this.element.tagName;
- }
- get _$AU() {
- return this._$AM._$AU;
- }
- constructor(e, s, a, i, r) {
- this.type = 1, this._$AH = l, this._$AN = void 0, this.element = e, this.name = s, this._$AM = i, this.options = r, a.length > 2 || a[0] !== "" || a[1] !== "" ? (this._$AH = Array(a.length - 1).fill(new String()), this.strings = a) : this._$AH = l;
- }
- _$AI(e, s = this, a, i) {
- const r = this.strings;
- let n = !1;
- if (r === void 0) e = N(this, e, s, 0), n = !G(e) || e !== this._$AH && e !== B, n && (this._$AH = e);
- else {
- const c = e;
- let d, h;
- for (e = r[0], d = 0; d < r.length - 1; d++) h = N(this, c[a + d], s, d), h === B && (h = this._$AH[d]), n || (n = !G(h) || h !== this._$AH[d]), h === l ? e = l : e !== l && (e += (h ?? "") + r[d + 1]), this._$AH[d] = h;
- }
- n && !i && this.j(e);
- }
- j(e) {
- e === l ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, e ?? "");
- }
-}
-class qe extends ee {
- constructor() {
- super(...arguments), this.type = 3;
- }
- j(e) {
- this.element[this.name] = e === l ? void 0 : e;
- }
-}
-class Ie extends ee {
- constructor() {
- super(...arguments), this.type = 4;
- }
- j(e) {
- this.element.toggleAttribute(this.name, !!e && e !== l);
- }
-}
-class Fe extends ee {
- constructor(e, s, a, i, r) {
- super(e, s, a, i, r), this.type = 5;
- }
- _$AI(e, s = this) {
- if ((e = N(this, e, s, 0) ?? l) === B) return;
- const a = this._$AH, i = e === l && a !== l || e.capture !== a.capture || e.once !== a.once || e.passive !== a.passive, r = e !== l && (a === l || i);
- i && this.element.removeEventListener(this.name, this, a), r && this.element.addEventListener(this.name, this, e), this._$AH = e;
- }
- handleEvent(e) {
- var s;
- typeof this._$AH == "function" ? this._$AH.call(((s = this.options) == null ? void 0 : s.host) ?? this.element, e) : this._$AH.handleEvent(e);
- }
-}
-class Ge {
- constructor(e, s, a) {
- this.element = e, this.type = 6, this._$AN = void 0, this._$AM = s, this.options = a;
- }
- get _$AU() {
- return this._$AM._$AU;
- }
- _$AI(e) {
- N(this, e);
- }
-}
-const ae = I.litHtmlPolyfillSupport;
-ae == null || ae(V, J), (I.litHtmlVersions ?? (I.litHtmlVersions = [])).push("3.3.2");
-const Ve = (t, e, s) => {
- const a = (s == null ? void 0 : s.renderBefore) ?? e;
- let i = a._$litPart$;
- if (i === void 0) {
- const r = (s == null ? void 0 : s.renderBefore) ?? null;
- a._$litPart$ = i = new J(e.insertBefore(F(), r), r, void 0, s ?? {});
- }
- return i._$AI(t), i;
-};
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-const U = globalThis;
-class A extends j {
- constructor() {
- super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
- }
- createRenderRoot() {
- var s;
- const e = super.createRenderRoot();
- return (s = this.renderOptions).renderBefore ?? (s.renderBefore = e.firstChild), e;
- }
- update(e) {
- const s = this.render();
- this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = Ve(s, this.renderRoot, this.renderOptions);
- }
- connectedCallback() {
- var e;
- super.connectedCallback(), (e = this._$Do) == null || e.setConnected(!0);
- }
- disconnectedCallback() {
- var e;
- super.disconnectedCallback(), (e = this._$Do) == null || e.setConnected(!1);
- }
- render() {
- return B;
- }
-}
-var we;
-A._$litElement$ = !0, A.finalized = !0, (we = U.litElementHydrateSupport) == null || we.call(U, { LitElement: A });
-const re = U.litElementPolyfillSupport;
-re == null || re({ LitElement: A });
-(U.litElementVersions ?? (U.litElementVersions = [])).push("4.2.2");
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-const Z = (t) => (e, s) => {
- s !== void 0 ? s.addInitializer(() => {
- customElements.define(t, e);
- }) : customElements.define(t, e);
-};
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-const Ke = { attribute: !0, type: String, converter: Q, reflect: !1, hasChanged: le }, Je = (t = Ke, e, s) => {
- const { kind: a, metadata: i } = s;
- let r = globalThis.litPropertyMetadata.get(i);
- if (r === void 0 && globalThis.litPropertyMetadata.set(i, r = /* @__PURE__ */ new Map()), a === "setter" && ((t = Object.create(t)).wrapped = !0), r.set(s.name, t), a === "accessor") {
- const { name: n } = s;
- return { set(c) {
- const d = e.get.call(this);
- e.set.call(this, c), this.requestUpdate(n, d, t, !0, c);
- }, init(c) {
- return c !== void 0 && this.C(n, void 0, t, c), c;
- } };
- }
- if (a === "setter") {
- const { name: n } = s;
- return function(c) {
- const d = this[n];
- e.call(this, c), this.requestUpdate(n, d, t, !0, c);
- };
- }
- throw Error("Unsupported decorator location: " + a);
-};
-function z(t) {
- return (e, s) => typeof s == "object" ? Je(t, e, s) : ((a, i, r) => {
- const n = i.hasOwnProperty(r);
- return i.constructor.createProperty(r, a), n ? Object.getOwnPropertyDescriptor(i, r) : void 0;
- })(t, e, s);
-}
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-function p(t) {
- return z({ ...t, state: !0, attribute: !1 });
-}
-function Ze(t, e) {
- const s = new WebSocket(t);
- return s.onmessage = (a) => {
- var i, r, n, c, d, h, u, f, b, v, w, W;
- try {
- const x = JSON.parse(a.data);
- ((r = (i = x.type) == null ? void 0 : i.startsWith) != null && r.call(i, "build.") || (c = (n = x.type) == null ? void 0 : n.startsWith) != null && c.call(n, "release.") || (h = (d = x.type) == null ? void 0 : d.startsWith) != null && h.call(d, "sdk.") || (f = (u = x.channel) == null ? void 0 : u.startsWith) != null && f.call(u, "build.") || (v = (b = x.channel) == null ? void 0 : b.startsWith) != null && v.call(b, "release.") || (W = (w = x.channel) == null ? void 0 : w.startsWith) != null && W.call(w, "sdk.")) && e(x);
- } catch {
- }
- }, s;
-}
-class te {
- constructor(e = "") {
- this.baseUrl = e;
- }
- get base() {
- return `${this.baseUrl}/api/v1/build`;
- }
- async request(e, s) {
- var r;
- const i = await (await fetch(`${this.base}${e}`, s)).json();
- if (!i.success)
- throw new Error(((r = i.error) == null ? void 0 : r.message) ?? "Request failed");
- return i.data;
- }
- // -- Build ------------------------------------------------------------------
- config() {
- return this.request("/config");
- }
- discover() {
- return this.request("/discover");
- }
- build() {
- return this.request("/build", { method: "POST" });
- }
- artifacts() {
- return this.request("/artifacts");
- }
- // -- Release ----------------------------------------------------------------
- version() {
- return this.request("/release/version");
- }
- changelog(e, s) {
- const a = new URLSearchParams();
- e && a.set("from", e), s && a.set("to", s);
- const i = a.toString();
- return this.request(`/release/changelog${i ? `?${i}` : ""}`);
- }
- release(e = !1) {
- const s = e ? "?dry_run=true" : "";
- return this.request(`/release${s}`, { method: "POST" });
- }
- releaseWorkflow(e = {}) {
- return this.request("/release/workflow", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(e)
- });
- }
- // -- SDK --------------------------------------------------------------------
- sdkDiff(e, s) {
- const a = new URLSearchParams({ base: e, revision: s });
- return this.request(`/sdk/diff?${a.toString()}`);
- }
- sdkGenerate(e) {
- const s = e ? JSON.stringify({ language: e }) : void 0;
- return this.request("/sdk/generate", {
- method: "POST",
- headers: s ? { "Content-Type": "application/json" } : void 0,
- body: s
- });
- }
-}
-var Xe = Object.defineProperty, Qe = Object.getOwnPropertyDescriptor, M = (t, e, s, a) => {
- for (var i = a > 1 ? void 0 : a ? Qe(e, s) : e, r = t.length - 1, n; r >= 0; r--)
- (n = t[r]) && (i = (a ? n(e, s, i) : n(i)) || i);
- return a && i && Xe(e, s, i), i;
-};
-let E = class extends A {
- constructor() {
- super(...arguments), this.apiUrl = "", this.configData = null, this.discoverData = null, this.loading = !0, this.error = "";
- }
- connectedCallback() {
- super.connectedCallback(), this.api = new te(this.apiUrl), this.reload();
- }
- async reload() {
- this.loading = !0, this.error = "";
- try {
- const [t, e] = await Promise.all([
- this.api.config(),
- this.api.discover()
- ]);
- this.configData = t, this.discoverData = e;
- } catch (t) {
- this.error = t.message ?? "Failed to load configuration";
- } finally {
- this.loading = !1;
- }
- }
- hasAppleConfig(t) {
- return t ? Object.entries(t).some(([, e]) => e == null ? !1 : Array.isArray(e) ? e.length > 0 : typeof e == "object" ? Object.keys(e).length > 0 : typeof e == "string" ? e.length > 0 : !0) : !1;
- }
- renderToggle(t, e, s = "Enabled", a = "Disabled") {
- return e == null ? l : o`
-
- ${t}
-
- ${e ? s : a}
-
-
- `;
- }
- renderFlags(t, e) {
- return !e || e.length === 0 ? l : o`
-
-
${t}
-
- ${e.map((s) => o`${s}`)}
-
-
- `;
- }
- render() {
- var a, i, r, n, c, d, h, u, f, b, v, w, W, x;
- if (this.loading)
- return o`Loading configuration\u2026
`;
- if (this.error)
- return o`${this.error}
`;
- if (!this.configData)
- return o`No configuration available.
`;
- const t = this.configData.config, e = this.discoverData, s = e ? e.has_subtree_package_json ?? e.has_subtree_npm ?? !1 : !1;
- return o`
-
-
-
Project Detection
-
- Config file
-
- ${this.configData.has_config ? "Present" : "Using defaults"}
-
-
- ${e ? o`
-
- Primary type
- ${e.primary || "none"}
-
-
- Suggested stack
- ${e.suggested_stack || e.primary_stack || e.primary || "none"}
-
- ${e.types.length > 1 ? o`
-
- Detected types
- ${e.types.join(", ")}
-
- ` : l}
-
- Frontend
-
- ${e.has_frontend ? "Detected" : "None"}
-
-
-
- Nested frontend
-
- ${s ? "Depth 2" : "None"}
-
-
- ${e.distro ? o`
-
- Distro
- ${e.distro}
-
- ` : l}
- ${e.linux_packages && e.linux_packages.length > 0 ? o`
-
-
Linux packages
-
- ${e.linux_packages.map((g) => o`${g}`)}
-
-
- ` : l}
- ${e.build_options ? o`
-
- Computed options
- ${e.build_options}
-
- ` : l}
- ${this.renderToggle("Computed obfuscation", (a = e.options) == null ? void 0 : a.obfuscate)}
- ${this.renderToggle("Computed NSIS", (i = e.options) == null ? void 0 : i.nsis)}
- ${(r = e.options) != null && r.webview2 ? o`
-
- Computed WebView2
- ${e.options.webview2}
-
- ` : l}
- ${this.renderFlags("Computed tags", (n = e.options) == null ? void 0 : n.tags)}
- ${this.renderFlags("Computed LD flags", (c = e.options) == null ? void 0 : c.ldflags)}
- ${e.ref ? o`
-
- Git ref
- ${e.ref}
-
- ` : l}
- ${e.branch ? o`
-
- Branch
- ${e.branch}
-
- ` : l}
- ${e.tag ? o`
-
- Tag
- ${e.tag}
-
- ` : l}
- ${e.short_sha ? o`
-
- Short SHA
- ${e.short_sha}
-
- ` : l}
-
- Directory
- ${e.dir}
-
- ` : l}
-
-
- ${e != null && e.setup_plan ? o`
-
-
Setup Plan
- ${this.renderFlags(
- "Toolchains",
- (d = e.setup_plan.steps) == null ? void 0 : d.map((g) => g.tool)
- )}
- ${this.renderFlags("Frontend dirs", e.setup_plan.frontend_dirs)}
- ${e.setup_plan.linux_packages && e.setup_plan.linux_packages.length > 0 ? o`
-
-
System packages
-
- ${e.setup_plan.linux_packages.map((g) => o`${g}`)}
-
-
- ` : l}
- ${e.setup_plan.steps && e.setup_plan.steps.length > 0 ? e.setup_plan.steps.map(
- (g) => o`
-
- ${g.tool}
- ${g.reason}
-
- `
- ) : o`
-
- Steps
- No setup required
-
- `}
-
- ` : l}
-
-
-
-
Project
- ${t.project.name ? o`
-
- Name
- ${t.project.name}
-
- ` : l}
- ${t.project.description ? o`
-
- Description
- ${t.project.description}
-
- ` : l}
- ${t.project.binary ? o`
-
- Binary
- ${t.project.binary}
-
- ` : l}
-
- Main
- ${t.project.main}
-
-
-
-
-
-
Build Settings
- ${t.build.type ? o`
-
- Type override
- ${t.build.type}
-
- ` : l}
-
- CGO
- ${t.build.cgo ? "Enabled" : "Disabled"}
-
- ${this.renderToggle("Obfuscation", t.build.obfuscate)}
- ${this.renderToggle("NSIS packaging", t.build.nsis)}
- ${t.build.webview2 ? o`
-
- WebView2 mode
- ${t.build.webview2}
-
- ` : l}
- ${t.build.deno_build ? o`
-
- Deno build
- ${t.build.deno_build}
-
- ` : l}
- ${t.build.archive_format ? o`
-
- Archive format
- ${t.build.archive_format}
-
- ` : l}
- ${this.renderFlags("Build tags", t.build.build_tags)}
- ${t.build.flags && t.build.flags.length > 0 ? o`
-
-
Flags
-
- ${t.build.flags.map((g) => o`${g}`)}
-
-
- ` : l}
- ${t.build.ldflags && t.build.ldflags.length > 0 ? o`
-
-
LD flags
-
- ${t.build.ldflags.map((g) => o`${g}`)}
-
-
- ` : l}
- ${this.renderFlags("Environment", t.build.env)}
- ${(h = t.build.cache) != null && h.enabled || (u = t.build.cache) != null && u.path || (f = t.build.cache) != null && f.paths && t.build.cache.paths.length > 0 ? o`
- ${this.renderToggle("Build cache", (b = t.build.cache) == null ? void 0 : b.enabled)}
- ${(v = t.build.cache) != null && v.path ? o`
-
- Cache path
- ${t.build.cache.path}
-
- ` : l}
- ${this.renderFlags("Cache paths", (w = t.build.cache) == null ? void 0 : w.paths)}
- ` : l}
- ${t.build.dockerfile ? o`
-
- Dockerfile
- ${t.build.dockerfile}
-
- ` : l}
- ${t.build.image ? o`
-
- Image
- ${t.build.image}
-
- ` : l}
- ${t.build.registry ? o`
-
- Registry
- ${t.build.registry}
-
- ` : l}
- ${this.renderFlags("Image tags", t.build.tags)}
- ${this.renderToggle("Push image", t.build.push)}
- ${this.renderToggle("Load image", t.build.load)}
- ${t.build.linuxkit_config ? o`
-
- LinuxKit config
- ${t.build.linuxkit_config}
-
- ` : l}
- ${this.renderFlags("LinuxKit formats", t.build.formats)}
-
-
-
-
-
Targets
-
- ${t.targets.map(
- (g) => o`${g.os}/${g.arch}`
- )}
-
-
-
- ${t.apple && this.hasAppleConfig(t.apple) ? o`
-
-
Apple Pipeline
- ${t.apple.bundle_id ? o`
-
- Bundle ID
- ${t.apple.bundle_id}
-
- ` : l}
- ${t.apple.team_id ? o`
-
- Team ID
- ${t.apple.team_id}
-
- ` : l}
- ${t.apple.arch ? o`
-
- Architecture
- ${t.apple.arch}
-
- ` : l}
- ${t.apple.bundle_display_name ? o`
-
- Display name
- ${t.apple.bundle_display_name}
-
- ` : l}
- ${t.apple.min_system_version ? o`
-
- Minimum macOS
- ${t.apple.min_system_version}
-
- ` : l}
- ${t.apple.category ? o`
-
- Category
- ${t.apple.category}
-
- ` : l}
- ${this.renderToggle("Sign", t.apple.sign)}
- ${this.renderToggle("Notarise", t.apple.notarise)}
- ${this.renderToggle("DMG", t.apple.dmg)}
- ${this.renderToggle("TestFlight", t.apple.testflight)}
- ${this.renderToggle("App Store", t.apple.appstore)}
- ${t.apple.metadata_path ? o`
-
- Metadata path
- ${t.apple.metadata_path}
-
- ` : l}
- ${t.apple.privacy_policy_url ? o`
-
- Privacy policy
- ${t.apple.privacy_policy_url}
-
- ` : l}
- ${t.apple.dmg_volume_name ? o`
-
- DMG volume
- ${t.apple.dmg_volume_name}
-
- ` : l}
- ${t.apple.dmg_background ? o`
-
- DMG background
- ${t.apple.dmg_background}
-
- ` : l}
- ${t.apple.entitlements_path ? o`
-
- Entitlements
- ${t.apple.entitlements_path}
-
- ` : l}
- ${(W = t.apple.xcode_cloud) != null && W.workflow ? o`
-
- Xcode Cloud workflow
- ${t.apple.xcode_cloud.workflow}
-
- ` : l}
- ${(x = t.apple.xcode_cloud) != null && x.triggers && t.apple.xcode_cloud.triggers.length > 0 ? o`
-
-
Xcode Cloud triggers
-
- ${t.apple.xcode_cloud.triggers.map((g) => {
- const Pe = g.branch ? `branch:${g.branch}` : g.tag ? `tag:${g.tag}` : "manual", Ce = g.action ?? "archive";
- return o`${Pe} → ${Ce}`;
- })}
-
-
- ` : l}
-
- ` : l}
- `;
- }
-};
-E.styles = K`
- :host {
- display: block;
- font-family: system-ui, -apple-system, sans-serif;
- }
-
- .section {
- border: 1px solid #e5e7eb;
- border-radius: 0.5rem;
- padding: 1rem;
- background: #fff;
- margin-bottom: 1rem;
- }
-
- .section-title {
- font-size: 0.75rem;
- font-weight: 700;
- color: #6b7280;
- text-transform: uppercase;
- letter-spacing: 0.025em;
- margin-bottom: 0.75rem;
- }
-
- .field {
- display: flex;
- justify-content: space-between;
- align-items: baseline;
- padding: 0.375rem 0;
- border-bottom: 1px solid #f3f4f6;
- }
-
- .field:last-child {
- border-bottom: none;
- }
-
- .field-label {
- font-size: 0.8125rem;
- font-weight: 500;
- color: #374151;
- }
-
- .field-value {
- font-size: 0.8125rem;
- font-family: monospace;
- color: #6b7280;
- max-width: 36rem;
- text-align: right;
- word-break: break-word;
- }
-
- .badge {
- display: inline-block;
- font-size: 0.6875rem;
- font-weight: 600;
- padding: 0.125rem 0.5rem;
- border-radius: 1rem;
- }
-
- .badge.present {
- background: #dcfce7;
- color: #166534;
- }
-
- .badge.absent {
- background: #fef3c7;
- color: #92400e;
- }
-
- .badge.type-go {
- background: #dbeafe;
- color: #1e40af;
- }
-
- .badge.type-wails {
- background: #f3e8ff;
- color: #6b21a8;
- }
-
- .badge.type-node {
- background: #dcfce7;
- color: #166534;
- }
-
- .badge.type-php {
- background: #fef3c7;
- color: #92400e;
- }
-
- .badge.type-docker {
- background: #e0e7ff;
- color: #3730a3;
- }
-
- .targets {
- display: flex;
- flex-wrap: wrap;
- gap: 0.375rem;
- margin-top: 0.25rem;
- }
-
- .target-badge {
- font-size: 0.75rem;
- padding: 0.125rem 0.5rem;
- background: #f3f4f6;
- border-radius: 0.25rem;
- font-family: monospace;
- color: #374151;
- }
-
- .flags {
- display: flex;
- flex-wrap: wrap;
- gap: 0.25rem;
- }
-
- .flag {
- font-size: 0.75rem;
- padding: 0.0625rem 0.375rem;
- background: #f9fafb;
- border: 1px solid #e5e7eb;
- border-radius: 0.25rem;
- font-family: monospace;
- color: #6b7280;
- }
-
- .empty {
- text-align: center;
- padding: 2rem;
- color: #9ca3af;
- font-size: 0.875rem;
- }
-
- .loading {
- text-align: center;
- padding: 2rem;
- color: #6b7280;
- }
-
- .error {
- color: #dc2626;
- padding: 0.75rem;
- background: #fef2f2;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- }
- `;
-M([
- z({ attribute: "api-url" })
-], E.prototype, "apiUrl", 2);
-M([
- p()
-], E.prototype, "configData", 2);
-M([
- p()
-], E.prototype, "discoverData", 2);
-M([
- p()
-], E.prototype, "loading", 2);
-M([
- p()
-], E.prototype, "error", 2);
-E = M([
- Z("core-build-config")
-], E);
-var Ye = Object.defineProperty, et = Object.getOwnPropertyDescriptor, S = (t, e, s, a) => {
- for (var i = a > 1 ? void 0 : a ? et(e, s) : e, r = t.length - 1, n; r >= 0; r--)
- (n = t[r]) && (i = (a ? n(e, s, i) : n(i)) || i);
- return a && i && Ye(e, s, i), i;
-};
-let _ = class extends A {
- constructor() {
- super(...arguments), this.apiUrl = "", this.artifacts = [], this.distExists = !1, this.loading = !0, this.error = "", this.building = !1, this.confirmBuild = !1, this.buildSuccess = "";
- }
- connectedCallback() {
- super.connectedCallback(), this.api = new te(this.apiUrl), this.reload();
- }
- async reload() {
- this.loading = !0, this.error = "";
- try {
- const t = await this.api.artifacts();
- this.artifacts = t.artifacts ?? [], this.distExists = t.exists ?? !1;
- } catch (t) {
- this.error = t.message ?? "Failed to load artifacts";
- } finally {
- this.loading = !1;
- }
- }
- handleBuildClick() {
- this.confirmBuild = !0, this.buildSuccess = "";
- }
- handleCancelBuild() {
- this.confirmBuild = !1;
- }
- async handleConfirmBuild() {
- var t;
- this.confirmBuild = !1, this.building = !0, this.error = "", this.buildSuccess = "";
- try {
- const e = await this.api.build();
- this.buildSuccess = `Build complete — ${((t = e.artifacts) == null ? void 0 : t.length) ?? 0} artifact(s) produced (${e.version})`, await this.reload();
- } catch (e) {
- this.error = e.message ?? "Build failed";
- } finally {
- this.building = !1;
- }
- }
- formatSize(t) {
- return t < 1024 ? `${t} B` : t < 1024 * 1024 ? `${(t / 1024).toFixed(1)} KB` : `${(t / (1024 * 1024)).toFixed(1)} MB`;
- }
- render() {
- return this.loading ? o`Loading artifacts\u2026
` : o`
-
-
- ${this.distExists ? `${this.artifacts.length} file(s) in dist/` : "No dist/ directory"}
-
-
-
-
- ${this.confirmBuild ? o`
-
- This will run a full build and overwrite dist/. Continue?
-
-
-
- ` : l}
-
- ${this.error ? o`${this.error}
` : l}
- ${this.buildSuccess ? o`${this.buildSuccess}
` : l}
-
- ${this.artifacts.length === 0 ? o`${this.distExists ? "dist/ is empty." : "Run a build to create artifacts."}
` : o`
-
- ${this.artifacts.map(
- (t) => o`
-
- ${t.name}
- ${this.formatSize(t.size)}
-
- `
- )}
-
- `}
- `;
- }
-};
-_.styles = K`
- :host {
- display: block;
- font-family: system-ui, -apple-system, sans-serif;
- }
-
- .toolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1rem;
- }
-
- .toolbar-info {
- font-size: 0.8125rem;
- color: #6b7280;
- }
-
- button.build {
- padding: 0.5rem 1.25rem;
- background: #6366f1;
- color: #fff;
- border: none;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.15s;
- }
-
- button.build:hover {
- background: #4f46e5;
- }
-
- button.build:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- .confirm {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.75rem 1rem;
- background: #fffbeb;
- border: 1px solid #fde68a;
- border-radius: 0.375rem;
- margin-bottom: 1rem;
- font-size: 0.8125rem;
- }
-
- .confirm-text {
- flex: 1;
- color: #92400e;
- }
-
- button.confirm-yes {
- padding: 0.375rem 1rem;
- background: #dc2626;
- color: #fff;
- border: none;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- cursor: pointer;
- }
-
- button.confirm-yes:hover {
- background: #b91c1c;
- }
-
- button.confirm-no {
- padding: 0.375rem 0.75rem;
- background: #fff;
- border: 1px solid #d1d5db;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- cursor: pointer;
- }
-
- .list {
- display: flex;
- flex-direction: column;
- gap: 0.375rem;
- }
-
- .artifact {
- border: 1px solid #e5e7eb;
- border-radius: 0.375rem;
- padding: 0.625rem 1rem;
- background: #fff;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- .artifact-name {
- font-size: 0.875rem;
- font-family: monospace;
- font-weight: 500;
- color: #111827;
- }
-
- .artifact-size {
- font-size: 0.75rem;
- color: #6b7280;
- }
-
- .empty {
- text-align: center;
- padding: 2rem;
- color: #9ca3af;
- font-size: 0.875rem;
- }
-
- .loading {
- text-align: center;
- padding: 2rem;
- color: #6b7280;
- }
-
- .error {
- color: #dc2626;
- padding: 0.75rem;
- background: #fef2f2;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- margin-bottom: 1rem;
- }
-
- .success {
- padding: 0.75rem;
- background: #f0fdf4;
- border: 1px solid #bbf7d0;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- color: #166534;
- margin-bottom: 1rem;
- }
- `;
-S([
- z({ attribute: "api-url" })
-], _.prototype, "apiUrl", 2);
-S([
- p()
-], _.prototype, "artifacts", 2);
-S([
- p()
-], _.prototype, "distExists", 2);
-S([
- p()
-], _.prototype, "loading", 2);
-S([
- p()
-], _.prototype, "error", 2);
-S([
- p()
-], _.prototype, "building", 2);
-S([
- p()
-], _.prototype, "confirmBuild", 2);
-S([
- p()
-], _.prototype, "buildSuccess", 2);
-_ = S([
- Z("core-build-artifacts")
-], _);
-var tt = Object.defineProperty, st = Object.getOwnPropertyDescriptor, y = (t, e, s, a) => {
- for (var i = a > 1 ? void 0 : a ? st(e, s) : e, r = t.length - 1, n; r >= 0; r--)
- (n = t[r]) && (i = (a ? n(e, s, i) : n(i)) || i);
- return a && i && tt(e, s, i), i;
-};
-let m = class extends A {
- constructor() {
- super(...arguments), this.apiUrl = "", this.version = "", this.changelog = "", this.loading = !0, this.error = "", this.releasing = !1, this.confirmRelease = !1, this.releaseSuccess = "", this.workflowPath = ".github/workflows/release.yml", this.workflowOutputPath = "", this.generatingWorkflow = !1, this.workflowSuccess = "";
- }
- connectedCallback() {
- super.connectedCallback(), this.api = new te(this.apiUrl), this.reload();
- }
- async reload() {
- this.loading = !0, this.error = "";
- try {
- const [t, e] = await Promise.all([
- this.api.version(),
- this.api.changelog()
- ]);
- this.version = t.version ?? "", this.changelog = e.changelog ?? "";
- } catch (t) {
- this.error = t.message ?? "Failed to load release information";
- } finally {
- this.loading = !1;
- }
- }
- handleReleaseClick() {
- this.confirmRelease = !0, this.releaseSuccess = "";
- }
- handleWorkflowPathInput(t) {
- const e = t.target;
- this.workflowPath = (e == null ? void 0 : e.value) ?? "";
- }
- handleWorkflowOutputPathInput(t) {
- const e = t.target;
- this.workflowOutputPath = (e == null ? void 0 : e.value) ?? "";
- }
- async handleGenerateWorkflow() {
- this.generatingWorkflow = !0, this.error = "", this.workflowSuccess = "";
- try {
- const t = {}, e = this.workflowPath.trim(), s = this.workflowOutputPath.trim();
- e && (t.path = e), e && (t.workflowPath = e, t.workflow_path = e, t["workflow-path"] = e), s && (t.outputPath = s), s && (t["output-path"] = s, t.output_path = s, t.output = s, t.workflowOutputPath = s, t.workflow_output = s, t["workflow-output"] = s, t.workflow_output_path = s, t["workflow-output-path"] = s);
- const i = (await this.api.releaseWorkflow(t)).path ?? s ?? e ?? ".github/workflows/release.yml";
- this.workflowSuccess = `Workflow generated at ${i}`;
- } catch (t) {
- this.error = t.message ?? "Failed to generate release workflow";
- } finally {
- this.generatingWorkflow = !1;
- }
- }
- handleCancelRelease() {
- this.confirmRelease = !1;
- }
- async handleConfirmRelease() {
- this.confirmRelease = !1, await this.doRelease(!1);
- }
- async handleDryRun() {
- await this.doRelease(!0);
- }
- async doRelease(t) {
- var e;
- this.releasing = !0, this.error = "", this.releaseSuccess = "";
- try {
- const s = await this.api.release(t), a = t ? "Dry run complete" : "Release published";
- this.releaseSuccess = `${a} — ${s.version} (${((e = s.artifacts) == null ? void 0 : e.length) ?? 0} artifact(s))`, await this.reload();
- } catch (s) {
- this.error = s.message ?? "Release failed";
- } finally {
- this.releasing = !1;
- }
- }
- render() {
- return this.loading ? o`Loading release information\u2026
` : o`
- ${this.error ? o`${this.error}
` : l}
- ${this.releaseSuccess ? o`${this.releaseSuccess}
` : l}
- ${this.workflowSuccess ? o`${this.workflowSuccess}
` : l}
-
-
-
-
Current Version
-
${this.version || "unknown"}
-
-
-
-
-
-
-
-
-
Release Workflow
-
-
-
-
Workflow Output Path
-
-
-
-
-
-
-
-
- ${this.confirmRelease ? o`
-
- This will publish ${this.version} to all configured targets. This action cannot be undone. Continue?
-
-
-
- ` : l}
-
- ${this.changelog ? o`
-
- ` : o`No changelog available.
`}
- `;
- }
-};
-m.styles = K`
- :host {
- display: block;
- font-family: system-ui, -apple-system, sans-serif;
- }
-
- .version-bar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 1rem;
- background: #fff;
- border: 1px solid #e5e7eb;
- border-radius: 0.5rem;
- margin-bottom: 1rem;
- }
-
- .version-label {
- font-size: 0.75rem;
- font-weight: 600;
- color: #6b7280;
- text-transform: uppercase;
- letter-spacing: 0.025em;
- }
-
- .version-value {
- font-size: 1.25rem;
- font-weight: 700;
- font-family: monospace;
- color: #111827;
- }
-
- .actions {
- display: flex;
- gap: 0.5rem;
- flex-wrap: wrap;
- }
-
- button {
- padding: 0.5rem 1rem;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- cursor: pointer;
- transition: background 0.15s;
- }
-
- button.release {
- background: #6366f1;
- color: #fff;
- border: none;
- font-weight: 500;
- }
-
- button.release:hover {
- background: #4f46e5;
- }
-
- button.release:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- button.dry-run {
- background: #fff;
- color: #6366f1;
- border: 1px solid #6366f1;
- }
-
- button.dry-run:hover {
- background: #eef2ff;
- }
-
- .workflow-section {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- padding: 0.875rem 1rem;
- background: linear-gradient(180deg, #fff, #f8fafc);
- border: 1px solid #e5e7eb;
- border-radius: 0.5rem;
- margin-bottom: 1rem;
- }
-
- .workflow-fields {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- }
-
- .workflow-field {
- display: flex;
- gap: 0.5rem;
- align-items: center;
- flex-wrap: wrap;
- }
-
- .workflow-field-label {
- min-width: 9rem;
- font-size: 0.8125rem;
- font-weight: 600;
- color: #374151;
- }
-
- .workflow-row {
- display: flex;
- gap: 0.5rem;
- align-items: center;
- flex-wrap: wrap;
- }
-
- .workflow-label {
- font-size: 0.75rem;
- font-weight: 700;
- color: #6b7280;
- text-transform: uppercase;
- letter-spacing: 0.025em;
- }
-
- .workflow-input {
- flex: 1;
- min-width: 16rem;
- padding: 0.5rem 0.75rem;
- border: 1px solid #d1d5db;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- font-family: monospace;
- color: #111827;
- background: #fff;
- }
-
- .workflow-input:focus {
- outline: none;
- border-color: #6366f1;
- box-shadow: 0 0 0 3px rgb(99 102 241 / 12%);
- }
-
- button.workflow {
- background: #111827;
- color: #fff;
- border: none;
- font-weight: 500;
- }
-
- button.workflow:hover {
- background: #1f2937;
- }
-
- button.workflow:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- .confirm {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.75rem 1rem;
- background: #fef2f2;
- border: 1px solid #fecaca;
- border-radius: 0.375rem;
- margin-bottom: 1rem;
- font-size: 0.8125rem;
- }
-
- .confirm-text {
- flex: 1;
- color: #991b1b;
- }
-
- button.confirm-yes {
- padding: 0.375rem 1rem;
- background: #dc2626;
- color: #fff;
- border: none;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- cursor: pointer;
- }
-
- button.confirm-no {
- padding: 0.375rem 0.75rem;
- background: #fff;
- border: 1px solid #d1d5db;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- cursor: pointer;
- }
-
- .changelog-section {
- border: 1px solid #e5e7eb;
- border-radius: 0.5rem;
- background: #fff;
- }
-
- .changelog-header {
- padding: 0.75rem 1rem;
- border-bottom: 1px solid #e5e7eb;
- font-size: 0.75rem;
- font-weight: 700;
- color: #6b7280;
- text-transform: uppercase;
- letter-spacing: 0.025em;
- }
-
- .changelog-content {
- padding: 1rem;
- font-size: 0.875rem;
- line-height: 1.6;
- white-space: pre-wrap;
- font-family: system-ui, -apple-system, sans-serif;
- color: #374151;
- max-height: 400px;
- overflow-y: auto;
- }
-
- .empty {
- text-align: center;
- padding: 2rem;
- color: #9ca3af;
- font-size: 0.875rem;
- }
-
- .loading {
- text-align: center;
- padding: 2rem;
- color: #6b7280;
- }
-
- .error {
- color: #dc2626;
- padding: 0.75rem;
- background: #fef2f2;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- margin-bottom: 1rem;
- }
-
- .success {
- padding: 0.75rem;
- background: #f0fdf4;
- border: 1px solid #bbf7d0;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- color: #166534;
- margin-bottom: 1rem;
- }
- `;
-y([
- z({ attribute: "api-url" })
-], m.prototype, "apiUrl", 2);
-y([
- p()
-], m.prototype, "version", 2);
-y([
- p()
-], m.prototype, "changelog", 2);
-y([
- p()
-], m.prototype, "loading", 2);
-y([
- p()
-], m.prototype, "error", 2);
-y([
- p()
-], m.prototype, "releasing", 2);
-y([
- p()
-], m.prototype, "confirmRelease", 2);
-y([
- p()
-], m.prototype, "releaseSuccess", 2);
-y([
- p()
-], m.prototype, "workflowPath", 2);
-y([
- p()
-], m.prototype, "workflowOutputPath", 2);
-y([
- p()
-], m.prototype, "generatingWorkflow", 2);
-y([
- p()
-], m.prototype, "workflowSuccess", 2);
-m = y([
- Z("core-build-release")
-], m);
-var it = Object.defineProperty, at = Object.getOwnPropertyDescriptor, k = (t, e, s, a) => {
- for (var i = a > 1 ? void 0 : a ? at(e, s) : e, r = t.length - 1, n; r >= 0; r--)
- (n = t[r]) && (i = (a ? n(e, s, i) : n(i)) || i);
- return a && i && it(e, s, i), i;
-};
-let $ = class extends A {
- constructor() {
- super(...arguments), this.apiUrl = "", this.basePath = "", this.revisionPath = "", this.diffResult = null, this.diffing = !1, this.diffError = "", this.selectedLanguage = "", this.generating = !1, this.generateError = "", this.generateSuccess = "";
- }
- connectedCallback() {
- super.connectedCallback(), this.api = new te(this.apiUrl);
- }
- async reload() {
- this.diffResult = null, this.diffError = "", this.generateError = "", this.generateSuccess = "";
- }
- async handleDiff() {
- if (!this.basePath.trim() || !this.revisionPath.trim()) {
- this.diffError = "Both base and revision spec paths are required.";
- return;
- }
- this.diffing = !0, this.diffError = "", this.diffResult = null;
- try {
- this.diffResult = await this.api.sdkDiff(this.basePath.trim(), this.revisionPath.trim());
- } catch (t) {
- this.diffError = t.message ?? "Diff failed";
- } finally {
- this.diffing = !1;
- }
- }
- async handleGenerate() {
- this.generating = !0, this.generateError = "", this.generateSuccess = "";
- try {
- const e = (await this.api.sdkGenerate(this.selectedLanguage || void 0)).language || "all languages";
- this.generateSuccess = `SDK generated successfully for ${e}.`;
- } catch (t) {
- this.generateError = t.message ?? "Generation failed";
- } finally {
- this.generating = !1;
- }
- }
- render() {
- return o`
-
-
-
OpenAPI Diff
-
-
- ${this.diffError ? o`
${this.diffError}
` : l}
-
- ${this.diffResult ? o`
-
-
${this.diffResult.Summary}
- ${this.diffResult.Changes && this.diffResult.Changes.length > 0 ? o`
-
- ${this.diffResult.Changes.map(
- (t) => o`- ${t}
`
- )}
-
- ` : l}
-
- ` : l}
-
-
-
-
-
SDK Generation
-
- ${this.generateError ? o`
${this.generateError}
` : l}
- ${this.generateSuccess ? o`
${this.generateSuccess}
` : l}
-
-
-
-
-
-
- `;
- }
-};
-$.styles = K`
- :host {
- display: block;
- font-family: system-ui, -apple-system, sans-serif;
- }
-
- .section {
- border: 1px solid #e5e7eb;
- border-radius: 0.5rem;
- padding: 1rem;
- background: #fff;
- margin-bottom: 1rem;
- }
-
- .section-title {
- font-size: 0.75rem;
- font-weight: 700;
- color: #6b7280;
- text-transform: uppercase;
- letter-spacing: 0.025em;
- margin-bottom: 0.75rem;
- }
-
- .diff-form {
- display: flex;
- gap: 0.5rem;
- align-items: flex-end;
- margin-bottom: 1rem;
- }
-
- .diff-field {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
- }
-
- .diff-field label {
- font-size: 0.75rem;
- font-weight: 500;
- color: #6b7280;
- }
-
- .diff-field input {
- padding: 0.375rem 0.75rem;
- border: 1px solid #d1d5db;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- font-family: monospace;
- }
-
- .diff-field input:focus {
- outline: none;
- border-color: #6366f1;
- box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
- }
-
- button {
- padding: 0.375rem 1rem;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- cursor: pointer;
- transition: background 0.15s;
- }
-
- button.primary {
- background: #6366f1;
- color: #fff;
- border: none;
- }
-
- button.primary:hover {
- background: #4f46e5;
- }
-
- button.primary:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- button.secondary {
- background: #fff;
- color: #374151;
- border: 1px solid #d1d5db;
- }
-
- button.secondary:hover {
- background: #f3f4f6;
- }
-
- .diff-result {
- padding: 0.75rem;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- margin-top: 0.75rem;
- }
-
- .diff-result.breaking {
- background: #fef2f2;
- border: 1px solid #fecaca;
- color: #991b1b;
- }
-
- .diff-result.safe {
- background: #f0fdf4;
- border: 1px solid #bbf7d0;
- color: #166534;
- }
-
- .diff-summary {
- font-weight: 600;
- margin-bottom: 0.5rem;
- }
-
- .diff-changes {
- list-style: disc;
- padding-left: 1.25rem;
- margin: 0;
- }
-
- .diff-changes li {
- font-size: 0.8125rem;
- margin-bottom: 0.25rem;
- font-family: monospace;
- }
-
- .generate-form {
- display: flex;
- gap: 0.5rem;
- align-items: center;
- }
-
- .generate-form select {
- padding: 0.375rem 0.75rem;
- border: 1px solid #d1d5db;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- background: #fff;
- }
-
- .empty {
- text-align: center;
- padding: 2rem;
- color: #9ca3af;
- font-size: 0.875rem;
- }
-
- .error {
- color: #dc2626;
- padding: 0.75rem;
- background: #fef2f2;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- margin-bottom: 1rem;
- }
-
- .success {
- padding: 0.75rem;
- background: #f0fdf4;
- border: 1px solid #bbf7d0;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- color: #166534;
- margin-bottom: 1rem;
- }
-
- .loading {
- text-align: center;
- padding: 1rem;
- color: #6b7280;
- font-size: 0.875rem;
- }
- `;
-k([
- z({ attribute: "api-url" })
-], $.prototype, "apiUrl", 2);
-k([
- p()
-], $.prototype, "basePath", 2);
-k([
- p()
-], $.prototype, "revisionPath", 2);
-k([
- p()
-], $.prototype, "diffResult", 2);
-k([
- p()
-], $.prototype, "diffing", 2);
-k([
- p()
-], $.prototype, "diffError", 2);
-k([
- p()
-], $.prototype, "selectedLanguage", 2);
-k([
- p()
-], $.prototype, "generating", 2);
-k([
- p()
-], $.prototype, "generateError", 2);
-k([
- p()
-], $.prototype, "generateSuccess", 2);
-$ = k([
- Z("core-build-sdk")
-], $);
-var rt = Object.defineProperty, nt = Object.getOwnPropertyDescriptor, H = (t, e, s, a) => {
- for (var i = a > 1 ? void 0 : a ? nt(e, s) : e, r = t.length - 1, n; r >= 0; r--)
- (n = t[r]) && (i = (a ? n(e, s, i) : n(i)) || i);
- return a && i && rt(e, s, i), i;
-};
-let O = class extends A {
- constructor() {
- super(...arguments), this.apiUrl = "", this.wsUrl = "", this.activeTab = "config", this.wsConnected = !1, this.lastEvent = "", this.ws = null, this.tabs = [
- { id: "config", label: "Config" },
- { id: "build", label: "Build" },
- { id: "release", label: "Release" },
- { id: "sdk", label: "SDK" }
- ];
- }
- connectedCallback() {
- super.connectedCallback(), this.wsUrl && this.connectWs();
- }
- disconnectedCallback() {
- super.disconnectedCallback(), this.ws && (this.ws.close(), this.ws = null);
- }
- connectWs() {
- this.ws = Ze(this.wsUrl, (t) => {
- this.lastEvent = t.channel ?? t.type ?? "", this.requestUpdate();
- }), this.ws.onopen = () => {
- this.wsConnected = !0;
- }, this.ws.onclose = () => {
- this.wsConnected = !1;
- };
- }
- handleTabClick(t) {
- this.activeTab = t;
- }
- handleRefresh() {
- var e;
- const t = (e = this.shadowRoot) == null ? void 0 : e.querySelector(".content");
- if (t) {
- const s = t.firstElementChild;
- s && "reload" in s && s.reload();
- }
- }
- renderContent() {
- switch (this.activeTab) {
- case "config":
- return o``;
- case "build":
- return o``;
- case "release":
- return o``;
- case "sdk":
- return o``;
- default:
- return l;
- }
- }
- render() {
- const t = this.wsUrl ? this.wsConnected ? "connected" : "disconnected" : "idle";
- return o`
-
-
-
- ${this.tabs.map(
- (e) => o`
-
- `
- )}
-
-
- ${this.renderContent()}
-
-
- `;
- }
-};
-O.styles = K`
- :host {
- display: flex;
- flex-direction: column;
- font-family: system-ui, -apple-system, sans-serif;
- height: 100%;
- background: #fafafa;
- }
-
- /* H — Header */
- .header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.75rem 1rem;
- background: #fff;
- border-bottom: 1px solid #e5e7eb;
- }
-
- .title {
- font-weight: 700;
- font-size: 1rem;
- color: #111827;
- }
-
- .refresh-btn {
- padding: 0.375rem 0.75rem;
- border: 1px solid #d1d5db;
- border-radius: 0.375rem;
- background: #fff;
- font-size: 0.8125rem;
- cursor: pointer;
- transition: background 0.15s;
- }
-
- .refresh-btn:hover {
- background: #f3f4f6;
- }
-
- /* H-L — Tabs */
- .tabs {
- display: flex;
- gap: 0;
- background: #fff;
- border-bottom: 1px solid #e5e7eb;
- padding: 0 1rem;
- }
-
- .tab {
- padding: 0.625rem 1rem;
- font-size: 0.8125rem;
- font-weight: 500;
- color: #6b7280;
- cursor: pointer;
- border-bottom: 2px solid transparent;
- transition: all 0.15s;
- background: none;
- border-top: none;
- border-left: none;
- border-right: none;
- }
-
- .tab:hover {
- color: #374151;
- }
-
- .tab.active {
- color: #6366f1;
- border-bottom-color: #6366f1;
- }
-
- /* C — Content */
- .content {
- flex: 1;
- padding: 1rem;
- overflow-y: auto;
- }
-
- /* F — Footer / Status bar */
- .footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.5rem 1rem;
- background: #fff;
- border-top: 1px solid #e5e7eb;
- font-size: 0.75rem;
- color: #9ca3af;
- }
-
- .ws-status {
- display: flex;
- align-items: center;
- gap: 0.375rem;
- }
-
- .ws-dot {
- width: 0.5rem;
- height: 0.5rem;
- border-radius: 50%;
- }
-
- .ws-dot.connected {
- background: #22c55e;
- }
-
- .ws-dot.disconnected {
- background: #ef4444;
- }
-
- .ws-dot.idle {
- background: #d1d5db;
- }
- `;
-H([
- z({ attribute: "api-url" })
-], O.prototype, "apiUrl", 2);
-H([
- z({ attribute: "ws-url" })
-], O.prototype, "wsUrl", 2);
-H([
- p()
-], O.prototype, "activeTab", 2);
-H([
- p()
-], O.prototype, "wsConnected", 2);
-H([
- p()
-], O.prototype, "lastEvent", 2);
-O = H([
- Z("core-build-panel")
-], O);
-export {
- te as BuildApi,
- _ as BuildArtifacts,
- E as BuildConfig,
- O as BuildPanel,
- m as BuildRelease,
- $ as BuildSdk,
- Ze as connectBuildEvents
-};
diff --git a/pkg/build/apple.go b/pkg/build/apple.go
deleted file mode 100644
index ae27644..0000000
--- a/pkg/build/apple.go
+++ /dev/null
@@ -1,2461 +0,0 @@
-package build
-
-import (
- "context"
- "encoding/xml"
- "io/fs"
- "net/url"
- "sort"
- "strconv"
- "syscall"
- "time"
- "unicode"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-const (
- defaultAppleArch = "universal"
- defaultAppleMinSystemVersion = "13.0"
- defaultAppleCategory = "public.app-category.developer-tools"
- defaultDMGIconSize = 128
- defaultDMGWindowWidth = 640
- defaultDMGWindowHeight = 480
- notaryToolLogCommand = "lo" + "g"
-)
-
-// AppleOptions holds the resolved runtime settings for the macOS Apple pipeline.
-type AppleOptions struct {
- TeamID string `json:"team_id" yaml:"team_id"`
- BundleID string `json:"bundle_id" yaml:"bundle_id"`
- Arch string `json:"arch" yaml:"arch"`
- CertIdentity string `json:"cert_identity" yaml:"cert_identity"`
- ProfilePath string `json:"profile_path" yaml:"profile_path"`
- KeychainPath string `json:"keychain_path" yaml:"keychain_path"`
- MetadataPath string `json:"metadata_path" yaml:"metadata_path"`
-
- Sign bool `json:"sign" yaml:"sign"`
- Notarise bool `json:"notarise" yaml:"notarise"`
- DMG bool `json:"dmg" yaml:"dmg"`
- TestFlight bool `json:"testflight" yaml:"testflight"`
- AppStore bool `json:"appstore" yaml:"appstore"`
-
- APIKeyID string `json:"api_key_id" yaml:"api_key_id"`
- APIKeyIssuerID string `json:"api_key_issuer_id" yaml:"api_key_issuer_id"`
- APIKeyPath string `json:"api_key_path" yaml:"api_key_path"`
- AppleID string `json:"apple_id" yaml:"apple_id"`
- Password string `json:"password" yaml:"password"`
-
- BundleDisplayName string `json:"bundle_display_name" yaml:"bundle_display_name"`
- MinSystemVersion string `json:"min_system_version" yaml:"min_system_version"`
- Category string `json:"category" yaml:"category"`
- Copyright string `json:"copyright" yaml:"copyright"`
- PrivacyPolicyURL string `json:"privacy_policy_url" yaml:"privacy_policy_url"`
- DMGBackground string `json:"dmg_background" yaml:"dmg_background"`
- DMGVolumeName string `json:"dmg_volume_name" yaml:"dmg_volume_name"`
- EntitlementsPath string `json:"entitlements_path" yaml:"entitlements_path"`
-}
-
-// AppleBuildResult captures the primary outputs of the Apple pipeline.
-type AppleBuildResult struct {
- BundlePath string
- DMGPath string
- DistributionPath string
- InfoPlistPath string
- EntitlementsPath string
- BuildNumber string
- Version string
-}
-
-// WailsBuildConfig defines the Wails v3 build inputs for a macOS app bundle.
-type WailsBuildConfig struct {
- ProjectDir string `json:"project_dir" yaml:"project_dir"`
- Name string `json:"name" yaml:"name"`
- Arch string `json:"arch" yaml:"arch"`
- BuildTags []string `json:"build_tags" yaml:"build_tags"`
- LDFlags []string `json:"ldflags" yaml:"ldflags"`
- OutputDir string `json:"output_dir" yaml:"output_dir"`
- Version string `json:"version" yaml:"version"`
- Env []string `json:"env" yaml:"env"`
- DenoBuild string `json:"deno_build" yaml:"deno_build"`
-}
-
-// SignConfig defines the codesign inputs for a macOS app bundle.
-type SignConfig struct {
- AppPath string `json:"app_path" yaml:"app_path"`
- Identity string `json:"identity" yaml:"identity"`
- Entitlements string `json:"entitlements" yaml:"entitlements"`
- Hardened bool `json:"hardened" yaml:"hardened"`
- Deep bool `json:"deep" yaml:"deep"`
- KeychainPath string `json:"keychain_path" yaml:"keychain_path"`
-}
-
-// NotariseConfig defines the Apple notarisation request.
-type NotariseConfig struct {
- AppPath string `json:"app_path" yaml:"app_path"`
-
- APIKeyID string `json:"api_key_id" yaml:"api_key_id"`
- APIKeyIssuerID string `json:"api_key_issuer_id" yaml:"api_key_issuer_id"`
- APIKeyPath string `json:"api_key_path" yaml:"api_key_path"`
-
- TeamID string `json:"team_id" yaml:"team_id"`
- AppleID string `json:"apple_id" yaml:"apple_id"`
- Password string `json:"password" yaml:"password"`
-}
-
-// DMGConfig defines the DMG packaging inputs.
-type DMGConfig struct {
- AppPath string `json:"app_path" yaml:"app_path"`
- OutputPath string `json:"output_path" yaml:"output_path"`
- VolumeName string `json:"volume_name" yaml:"volume_name"`
- Background string `json:"background" yaml:"background"`
- IconSize int `json:"icon_size" yaml:"icon_size"`
- WindowSize [2]int `json:"window_size" yaml:"window_size"`
-}
-
-// TestFlightConfig defines the TestFlight upload inputs.
-type TestFlightConfig struct {
- AppPath string `json:"app_path" yaml:"app_path"`
- APIKeyID string `json:"api_key_id" yaml:"api_key_id"`
- APIKeyIssuerID string `json:"api_key_issuer_id" yaml:"api_key_issuer_id"`
- APIKeyPath string `json:"api_key_path" yaml:"api_key_path"`
- CertIdentity string `json:"cert_identity" yaml:"cert_identity"`
-}
-
-// AppStoreConfig defines the App Store Connect submission inputs.
-type AppStoreConfig struct {
- AppPath string `json:"app_path" yaml:"app_path"`
- APIKeyID string `json:"api_key_id" yaml:"api_key_id"`
- APIKeyIssuerID string `json:"api_key_issuer_id" yaml:"api_key_issuer_id"`
- APIKeyPath string `json:"api_key_path" yaml:"api_key_path"`
- CertIdentity string `json:"cert_identity" yaml:"cert_identity"`
- Version string `json:"version" yaml:"version"`
- ReleaseType string `json:"release_type" yaml:"release_type"`
-}
-
-// InfoPlist defines the generated macOS application metadata.
-type InfoPlist struct {
- BundleID string `json:"bundle_id" plist:"CFBundleIdentifier"`
- BundleName string `json:"bundle_name" plist:"CFBundleName"`
- BundleDisplayName string `json:"bundle_display_name" plist:"CFBundleDisplayName"`
- BundleVersion string `json:"bundle_version" plist:"CFBundleShortVersionString"`
- BuildNumber string `json:"build_number" plist:"CFBundleVersion"`
- MinSystemVersion string `json:"min_system_version" plist:"LSMinimumSystemVersion"`
- Category string `json:"category" plist:"LSApplicationCategoryType"`
- Copyright string `json:"copyright" plist:"NSHumanReadableCopyright"`
- Executable string `json:"executable" plist:"CFBundleExecutable"`
- HighResCapable bool `json:"high_res_capable" plist:"NSHighResolutionCapable"`
- SupportsSecureRestorableState bool `json:"supports_secure_restorable_state" plist:"NSSupportsSecureRestorableState"`
-}
-
-// Entitlements defines the generated macOS entitlements profile.
-type Entitlements struct {
- Sandbox bool `json:"sandbox" plist:"com.apple.security.app-sandbox"`
- NetworkClient bool `json:"network_client" plist:"com.apple.security.network.client"`
- NetworkServer bool `json:"network_server" plist:"com.apple.security.network.server"`
- MetalGPU bool `json:"metal_gpu" plist:"com.apple.security.device.metal"`
- UserSelectedReadWrite bool `json:"user_selected_read_write" plist:"com.apple.security.files.user-selected.read-write"`
- Downloads bool `json:"downloads" plist:"com.apple.security.files.downloads.read-write"`
- HardenedRuntime bool `json:"hardened_runtime" plist:"com.apple.security.cs.allow-unsigned-executable-memory"`
- JIT bool `json:"jit" plist:"com.apple.security.cs.allow-jit"`
- DylibEnvVar bool `json:"dylib_env_var" plist:"com.apple.security.cs.allow-dylib-environment-variables"`
-}
-
-var (
- appleBuildWailsAppFn = BuildWailsApp
- appleCreateUniversalFn = CreateUniversal
- appleSignFn = Sign
- appleNotariseFn = Notarise
- appleCreateDMGFn = CreateDMG
- appleUploadTestFlightFn = UploadTestFlight
- appleSubmitAppStoreFn = SubmitAppStore
- appleResolveCommand = ax.ResolveCommand
- appleCombinedOutput = ax.CombinedOutput
-)
-
-// DefaultAppleOptions returns the runtime defaults for the Apple build pipeline.
-func DefaultAppleOptions() AppleOptions {
- return AppleOptions{
- Arch: defaultAppleArch,
- Sign: true,
- Notarise: true,
- MinSystemVersion: defaultAppleMinSystemVersion,
- Category: defaultAppleCategory,
- }
-}
-
-// Resolve materialises a config-backed Apple runtime option set.
-func (cfg AppleConfig) Resolve() AppleOptions {
- options := DefaultAppleOptions()
-
- if cfg.TeamID != "" {
- options.TeamID = cfg.TeamID
- }
- if cfg.BundleID != "" {
- options.BundleID = cfg.BundleID
- }
- if cfg.Arch != "" {
- options.Arch = cfg.Arch
- }
- if cfg.CertIdentity != "" {
- options.CertIdentity = cfg.CertIdentity
- }
- if cfg.ProfilePath != "" {
- options.ProfilePath = cfg.ProfilePath
- }
- if cfg.KeychainPath != "" {
- options.KeychainPath = cfg.KeychainPath
- }
- if cfg.MetadataPath != "" {
- options.MetadataPath = cfg.MetadataPath
- }
- if cfg.Sign != nil {
- options.Sign = *cfg.Sign
- }
- if cfg.Notarise != nil {
- options.Notarise = *cfg.Notarise
- }
- if cfg.DMG != nil {
- options.DMG = *cfg.DMG
- }
- if cfg.TestFlight != nil {
- options.TestFlight = *cfg.TestFlight
- }
- if cfg.AppStore != nil {
- options.AppStore = *cfg.AppStore
- }
- if cfg.APIKeyID != "" {
- options.APIKeyID = cfg.APIKeyID
- }
- if cfg.APIKeyIssuerID != "" {
- options.APIKeyIssuerID = cfg.APIKeyIssuerID
- }
- if cfg.APIKeyPath != "" {
- options.APIKeyPath = cfg.APIKeyPath
- }
- if cfg.AppleID != "" {
- options.AppleID = cfg.AppleID
- }
- if cfg.Password != "" {
- options.Password = cfg.Password
- }
- if cfg.BundleDisplayName != "" {
- options.BundleDisplayName = cfg.BundleDisplayName
- }
- if cfg.MinSystemVersion != "" {
- options.MinSystemVersion = cfg.MinSystemVersion
- }
- if cfg.Category != "" {
- options.Category = cfg.Category
- }
- if cfg.Copyright != "" {
- options.Copyright = cfg.Copyright
- }
- if cfg.PrivacyPolicyURL != "" {
- options.PrivacyPolicyURL = cfg.PrivacyPolicyURL
- }
- if cfg.DMGBackground != "" {
- options.DMGBackground = cfg.DMGBackground
- }
- if cfg.DMGVolumeName != "" {
- options.DMGVolumeName = cfg.DMGVolumeName
- }
- if cfg.EntitlementsPath != "" {
- options.EntitlementsPath = cfg.EntitlementsPath
- }
-
- return options
-}
-
-func validateAppleBuildOptions(options AppleOptions) core.Result {
- if options.Sign && core.Trim(options.CertIdentity) == "" {
- return core.Fail(core.E("build.validateAppleBuildOptions", "signing identity is required when sign is enabled", nil))
- }
-
- if options.Notarise {
- authArgs := notariseAuthArgs(NotariseConfig{
- AppPath: "",
- APIKeyID: options.APIKeyID,
- APIKeyIssuerID: options.APIKeyIssuerID,
- APIKeyPath: options.APIKeyPath,
- TeamID: options.TeamID,
- AppleID: options.AppleID,
- Password: options.Password,
- })
- if !authArgs.OK {
- return core.Fail(core.E("build.validateAppleBuildOptions", "invalid notarisation credentials", core.NewError(authArgs.Error())))
- }
- }
-
- if options.TestFlight || options.AppStore {
- valid := validateAppStoreConnectAPIKey(options.APIKeyID, options.APIKeyIssuerID, options.APIKeyPath, "build.validateAppleBuildOptions")
- if !valid.OK {
- return valid
- }
- if core.Trim(options.ProfilePath) == "" {
- return core.Fail(core.E("build.validateAppleBuildOptions", "profile_path is required for App Store Connect uploads", nil))
- }
- if isDeveloperIDIdentity(options.CertIdentity) {
- return core.Fail(core.E("build.validateAppleBuildOptions", "TestFlight and App Store uploads require an Apple distribution certificate, not Developer ID", nil))
- }
- }
-
- if options.AppStore {
- minSystemVersion := firstNonEmpty(options.MinSystemVersion, defaultAppleMinSystemVersion)
- if compareAppleVersion(minSystemVersion, defaultAppleMinSystemVersion) < 0 {
- return core.Fail(core.E("build.validateAppleBuildOptions", "App Store submissions require min_system_version 13.0 or newer", nil))
- }
-
- if core.Trim(firstNonEmpty(options.Category, defaultAppleCategory)) == "" {
- return core.Fail(core.E("build.validateAppleBuildOptions", "App Store submissions require an application category", nil))
- }
-
- if !core.Contains(core.Lower(options.Copyright), "eupl-1.2") {
- return core.Fail(core.E("build.validateAppleBuildOptions", "App Store submissions must declare EUPL-1.2 in copyright metadata", nil))
- }
-
- valid := validatePrivacyPolicyURL(options.PrivacyPolicyURL)
- if !valid.OK {
- return valid
- }
- }
-
- return core.Ok(nil)
-}
-
-// BuildApple runs the end-to-end macOS Apple pipeline for a Wails app.
-func BuildApple(ctx context.Context, cfg *Config, options AppleOptions, buildNumber string) core.Result {
- if cfg == nil {
- return core.Fail(core.E("build.BuildApple", "config is nil", nil))
- }
- if cfg.FS == nil {
- cfg.FS = storage.Local
- }
-
- if options.BundleID == "" {
- return core.Fail(core.E("build.BuildApple", "bundle_id is required for Apple builds", nil))
- }
- if options.Notarise && !options.Sign {
- return core.Fail(core.E("build.BuildApple", "notarisation requires code signing", nil))
- }
- if (options.TestFlight || options.AppStore) && !options.Sign {
- return core.Fail(core.E("build.BuildApple", "TestFlight and App Store uploads require code signing", nil))
- }
- valid := validateAppleBuildOptions(options)
- if !valid.OK {
- return valid
- }
-
- name := resolveAppleBundleName(cfg)
- outputDir := resolveAppleOutputDir(cfg)
- created := cfg.FS.EnsureDir(outputDir)
- if !created.OK {
- return core.Fail(core.E("build.BuildApple", "failed to create Apple output directory", core.NewError(created.Error())))
- }
-
- if buildNumber == "" {
- buildNumber = "1"
- }
-
- buildTags := deduplicateStrings(append(append([]string{}, cfg.BuildTags...), "mlx"))
- ldflags := append([]string{}, cfg.LDFlags...)
- version := cfg.Version
-
- var bundlePath string
- if options.Arch == "" {
- options.Arch = defaultAppleArch
- }
-
- switch options.Arch {
- case "universal":
- arm64Temp := ax.TempDir("core-build-apple-arm64-*")
- if !arm64Temp.OK {
- return core.Fail(core.E("build.BuildApple", "failed to create arm64 temp directory", core.NewError(arm64Temp.Error())))
- }
- arm64Dir := arm64Temp.Value.(string)
- defer ax.RemoveAll(arm64Dir)
-
- amd64Temp := ax.TempDir("core-build-apple-amd64-*")
- if !amd64Temp.OK {
- return core.Fail(core.E("build.BuildApple", "failed to create amd64 temp directory", core.NewError(amd64Temp.Error())))
- }
- amd64Dir := amd64Temp.Value.(string)
- defer ax.RemoveAll(amd64Dir)
-
- arm64BundleResult := appleBuildWailsAppFn(ctx, WailsBuildConfig{
- ProjectDir: cfg.ProjectDir,
- Name: name,
- Arch: "arm64",
- BuildTags: buildTags,
- LDFlags: ldflags,
- OutputDir: arm64Dir,
- Version: version,
- Env: BuildEnvironment(cfg),
- DenoBuild: cfg.DenoBuild,
- })
- if !arm64BundleResult.OK {
- return core.Fail(core.E("build.BuildApple", "failed to build arm64 bundle", core.NewError(arm64BundleResult.Error())))
- }
- arm64Bundle := arm64BundleResult.Value.(string)
-
- amd64BundleResult := appleBuildWailsAppFn(ctx, WailsBuildConfig{
- ProjectDir: cfg.ProjectDir,
- Name: name,
- Arch: "amd64",
- BuildTags: buildTags,
- LDFlags: ldflags,
- OutputDir: amd64Dir,
- Version: version,
- Env: BuildEnvironment(cfg),
- DenoBuild: cfg.DenoBuild,
- })
- if !amd64BundleResult.OK {
- return core.Fail(core.E("build.BuildApple", "failed to build amd64 bundle", core.NewError(amd64BundleResult.Error())))
- }
- amd64Bundle := amd64BundleResult.Value.(string)
-
- bundlePath = ax.Join(outputDir, name+".app")
- createdUniversal := appleCreateUniversalFn(arm64Bundle, amd64Bundle, bundlePath)
- if !createdUniversal.OK {
- return core.Fail(core.E("build.BuildApple", "failed to create universal app bundle", core.NewError(createdUniversal.Error())))
- }
- case "arm64", "amd64":
- bundleResult := appleBuildWailsAppFn(ctx, WailsBuildConfig{
- ProjectDir: cfg.ProjectDir,
- Name: name,
- Arch: options.Arch,
- BuildTags: buildTags,
- LDFlags: ldflags,
- OutputDir: outputDir,
- Version: version,
- Env: BuildEnvironment(cfg),
- DenoBuild: cfg.DenoBuild,
- })
- if !bundleResult.OK {
- return core.Fail(core.E("build.BuildApple", "failed to build app bundle", core.NewError(bundleResult.Error())))
- }
- bundlePath = bundleResult.Value.(string)
- default:
- return core.Fail(core.E("build.BuildApple", "unsupported Apple arch: "+options.Arch, nil))
- }
-
- infoPlist := InfoPlist{
- BundleID: options.BundleID,
- BundleName: name,
- BundleDisplayName: firstNonEmpty(options.BundleDisplayName, name),
- BundleVersion: normalizeAppleVersion(version),
- BuildNumber: buildNumber,
- MinSystemVersion: firstNonEmpty(options.MinSystemVersion, defaultAppleMinSystemVersion),
- Category: firstNonEmpty(options.Category, defaultAppleCategory),
- Copyright: options.Copyright,
- Executable: name,
- HighResCapable: true,
- SupportsSecureRestorableState: true,
- }
-
- infoPlistResult := WriteInfoPlist(cfg.FS, bundlePath, infoPlist)
- if !infoPlistResult.OK {
- return core.Fail(core.E("build.BuildApple", "failed to write Info.plist", core.NewError(infoPlistResult.Error())))
- }
- infoPlistPath := infoPlistResult.Value.(string)
-
- if options.ProfilePath != "" {
- copied := copyPath(cfg.FS, options.ProfilePath, ax.Join(bundlePath, "Contents", "embedded.provisionprofile"))
- if !copied.OK {
- return core.Fail(core.E("build.BuildApple", "failed to copy provisioning profile", core.NewError(copied.Error())))
- }
- }
-
- entitlementsPath := options.EntitlementsPath
- if entitlementsPath == "" {
- entitlementsPath = ax.Join(outputDir, name+".entitlements")
- }
- entitlements := directDistributionEntitlements()
- if options.AppStore || options.TestFlight {
- entitlements = appStoreEntitlements()
- }
- entitlementsResult := WriteEntitlements(cfg.FS, entitlementsPath, entitlements)
- if !entitlementsResult.OK {
- return core.Fail(core.E("build.BuildApple", "failed to write entitlements", core.NewError(entitlementsResult.Error())))
- }
-
- if options.Sign {
- signed := appleSignFn(ctx, SignConfig{
- AppPath: bundlePath,
- Identity: options.CertIdentity,
- Entitlements: entitlementsPath,
- Hardened: true,
- Deep: false,
- KeychainPath: options.KeychainPath,
- })
- if !signed.OK {
- return core.Fail(core.E("build.BuildApple", "failed to sign app bundle", core.NewError(signed.Error())))
- }
- }
-
- distributionPath := bundlePath
- dmgPath := ""
- if options.DMG {
- dmgPath = ax.Join(outputDir, core.Sprintf("%s-%s.dmg", name, normalizeAppleVersion(version)))
- createdDMG := appleCreateDMGFn(ctx, DMGConfig{
- AppPath: bundlePath,
- OutputPath: dmgPath,
- VolumeName: firstNonEmpty(options.DMGVolumeName, name),
- Background: options.DMGBackground,
- IconSize: 128,
- WindowSize: [2]int{640, 480},
- })
- if !createdDMG.OK {
- return core.Fail(core.E("build.BuildApple", "failed to create DMG", core.NewError(createdDMG.Error())))
- }
- if options.Sign {
- signed := appleSignFn(ctx, SignConfig{
- AppPath: dmgPath,
- Identity: options.CertIdentity,
- Hardened: false,
- Deep: false,
- KeychainPath: options.KeychainPath,
- })
- if !signed.OK {
- return core.Fail(core.E("build.BuildApple", "failed to sign DMG", core.NewError(signed.Error())))
- }
- }
- distributionPath = dmgPath
- }
-
- if options.Notarise {
- notarised := appleNotariseFn(ctx, NotariseConfig{
- AppPath: distributionPath,
- APIKeyID: options.APIKeyID,
- APIKeyIssuerID: options.APIKeyIssuerID,
- APIKeyPath: options.APIKeyPath,
- TeamID: options.TeamID,
- AppleID: options.AppleID,
- Password: options.Password,
- })
- if !notarised.OK {
- return core.Fail(core.E("build.BuildApple", "failed to notarise distribution", core.NewError(notarised.Error())))
- }
- }
-
- if options.TestFlight {
- uploaded := appleUploadTestFlightFn(ctx, TestFlightConfig{
- AppPath: bundlePath,
- APIKeyID: options.APIKeyID,
- APIKeyIssuerID: options.APIKeyIssuerID,
- APIKeyPath: options.APIKeyPath,
- CertIdentity: options.CertIdentity,
- })
- if !uploaded.OK {
- return core.Fail(core.E("build.BuildApple", "failed to upload TestFlight build", core.NewError(uploaded.Error())))
- }
- }
-
- if options.AppStore {
- preflight := validateAppStorePreflight(cfg.FS, cfg.ProjectDir, bundlePath, options)
- if !preflight.OK {
- return preflight
- }
-
- submitted := appleSubmitAppStoreFn(ctx, AppStoreConfig{
- AppPath: bundlePath,
- APIKeyID: options.APIKeyID,
- APIKeyIssuerID: options.APIKeyIssuerID,
- APIKeyPath: options.APIKeyPath,
- CertIdentity: options.CertIdentity,
- Version: normalizeAppleVersion(version),
- ReleaseType: "manual",
- })
- if !submitted.OK {
- return core.Fail(core.E("build.BuildApple", "failed to submit App Store build", core.NewError(submitted.Error())))
- }
- }
-
- return core.Ok(&AppleBuildResult{
- BundlePath: bundlePath,
- DMGPath: dmgPath,
- DistributionPath: distributionPath,
- InfoPlistPath: infoPlistPath,
- EntitlementsPath: entitlementsPath,
- BuildNumber: buildNumber,
- Version: normalizeAppleVersion(version),
- })
-}
-
-// BuildWailsApp builds a single-architecture Wails app bundle for macOS.
-func BuildWailsApp(ctx context.Context, cfg WailsBuildConfig) core.Result {
- if cfg.ProjectDir == "" {
- return core.Fail(core.E("build.BuildWailsApp", "project directory is required", nil))
- }
-
- name := cfg.Name
- if name == "" {
- name = ax.Base(cfg.ProjectDir)
- }
- if cfg.Arch == "" {
- return core.Fail(core.E("build.BuildWailsApp", "arch is required", nil))
- }
-
- prepared := prepareWailsFrontend(ctx, cfg)
- if !prepared.OK {
- return prepared
- }
-
- wailsCommandResult := resolveWails3Cli()
- if !wailsCommandResult.OK {
- return wailsCommandResult
- }
- wailsCommand := wailsCommandResult.Value.(string)
-
- args := []string{"build", "-platform", "darwin/" + cfg.Arch}
-
- buildTags := deduplicateStrings(append(append([]string{}, cfg.BuildTags...), "mlx"))
- if len(buildTags) > 0 {
- args = append(args, "-tags", core.Join(",", buildTags...))
- }
-
- ldflags := append([]string{}, cfg.LDFlags...)
- if cfg.Version != "" && !appleHasVersionLDFlag(ldflags) {
- versionFlag := VersionLinkerFlag(cfg.Version)
- if !versionFlag.OK {
- return versionFlag
- }
- ldflags = append(ldflags, versionFlag.Value.(string))
- }
- if len(ldflags) > 0 {
- args = append(args, "-ldflags", core.Join(" ", ldflags...))
- }
-
- env := append([]string{}, cfg.Env...)
- env = appendEnvIfMissing(env, "CGO_ENABLED", "1")
-
- output := appleCombinedOutput(ctx, cfg.ProjectDir, env, wailsCommand, args...)
- if !output.OK {
- return core.Fail(core.E("build.BuildWailsApp", "wails build failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- sourcePathResult := findBuiltAppBundle(cfg.ProjectDir, name)
- if !sourcePathResult.OK {
- return sourcePathResult
- }
- sourcePath := sourcePathResult.Value.(string)
-
- if cfg.OutputDir == "" {
- return core.Ok(sourcePath)
- }
-
- created := storage.Local.EnsureDir(cfg.OutputDir)
- if !created.OK {
- return core.Fail(core.E("build.BuildWailsApp", "failed to create Wails output directory", core.NewError(created.Error())))
- }
-
- destPath := ax.Join(cfg.OutputDir, name+".app")
- if storage.Local.Exists(destPath) {
- deleted := storage.Local.DeleteAll(destPath)
- if !deleted.OK {
- return core.Fail(core.E("build.BuildWailsApp", "failed to replace existing app bundle", core.NewError(deleted.Error())))
- }
- }
- copied := copyPath(storage.Local, sourcePath, destPath)
- if !copied.OK {
- return core.Fail(core.E("build.BuildWailsApp", "failed to copy built app bundle", core.NewError(copied.Error())))
- }
-
- return core.Ok(destPath)
-}
-
-func prepareWailsFrontend(ctx context.Context, cfg WailsBuildConfig) core.Result {
- buildResult := resolveWailsFrontendBuild(cfg)
- if !buildResult.OK {
- return buildResult
- }
- frontendBuild := buildResult.Value.(wailsFrontendBuild)
- frontendDir := frontendBuild.dir
- command := frontendBuild.command
- args := frontendBuild.args
- if command == "" {
- return core.Ok(nil)
- }
-
- output := appleCombinedOutput(ctx, frontendDir, cfg.Env, command, args...)
- if !output.OK {
- return core.Fail(core.E("build.prepareWailsFrontend", command+" build failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-type wailsFrontendBuild struct {
- dir string
- command string
- args []string
-}
-
-func resolveWailsFrontendBuild(cfg WailsBuildConfig) core.Result {
- frontendDir := resolveFrontendDir(storage.Local, cfg.ProjectDir)
- if frontendDir == "" {
- if DenoRequested(cfg.DenoBuild) {
- frontendDir = cfg.ProjectDir
- if storage.Local.IsDir(ax.Join(cfg.ProjectDir, "frontend")) {
- frontendDir = ax.Join(cfg.ProjectDir, "frontend")
- }
- } else {
- return core.Ok(wailsFrontendBuild{})
- }
- }
-
- if hasDenoConfig(storage.Local, frontendDir) || DenoRequested(cfg.DenoBuild) {
- denoBuild := resolveDenoBuildCommand(cfg)
- if !denoBuild.OK {
- return denoBuild
- }
- resolved := denoBuild.Value.(commandArgs)
- return core.Ok(wailsFrontendBuild{dir: frontendDir, command: resolved.command, args: resolved.args})
- }
-
- if storage.Local.IsFile(ax.Join(frontendDir, "package.json")) {
- return resolvePackageManagerBuild(frontendDir, detectPackageManager(storage.Local, frontendDir))
- }
-
- return core.Ok(wailsFrontendBuild{})
-}
-
-func resolveFrontendDir(filesystem storage.Medium, projectDir string) string {
- frontendDir := ax.Join(projectDir, "frontend")
- if filesystem.IsDir(frontendDir) && (hasDenoConfig(filesystem, frontendDir) || filesystem.IsFile(ax.Join(frontendDir, "package.json"))) {
- return frontendDir
- }
-
- if hasDenoConfig(filesystem, projectDir) || filesystem.IsFile(ax.Join(projectDir, "package.json")) {
- return projectDir
- }
-
- if nested := resolveSubtreeFrontendDir(filesystem, projectDir); nested != "" {
- return nested
- }
-
- if DenoRequested("") {
- if filesystem.IsDir(frontendDir) {
- return frontendDir
- }
- return projectDir
- }
-
- return ""
-}
-
-func hasDenoConfig(filesystem storage.Medium, dir string) bool {
- return filesystem.IsFile(ax.Join(dir, "deno.json")) || filesystem.IsFile(ax.Join(dir, "deno.jsonc"))
-}
-
-func resolveSubtreeFrontendDir(filesystem storage.Medium, projectDir string) string {
- return findFrontendDir(filesystem, projectDir, 0)
-}
-
-func findFrontendDir(filesystem storage.Medium, dir string, depth int) string {
- if depth >= 2 {
- return ""
- }
-
- entriesResult := filesystem.List(dir)
- if !entriesResult.OK {
- return ""
- }
- entries := entriesResult.Value.([]fs.DirEntry)
-
- for _, entry := range entries {
- if !entry.IsDir() {
- continue
- }
-
- name := entry.Name()
- if name == "node_modules" || core.HasPrefix(name, ".") {
- continue
- }
-
- candidateDir := ax.Join(dir, name)
- if hasDenoConfig(filesystem, candidateDir) || filesystem.IsFile(ax.Join(candidateDir, "package.json")) {
- return candidateDir
- }
-
- if nested := findFrontendDir(filesystem, candidateDir, depth+1); nested != "" {
- return nested
- }
- }
-
- return ""
-}
-
-func resolvePackageManagerBuild(frontendDir, packageManager string) core.Result {
- switch packageManager {
- case "bun":
- command := resolveBunCli()
- if !command.OK {
- return command
- }
- return core.Ok(wailsFrontendBuild{dir: frontendDir, command: command.Value.(string), args: []string{"run", "build"}})
- case "pnpm":
- command := resolvePnpmCli()
- if !command.OK {
- return command
- }
- return core.Ok(wailsFrontendBuild{dir: frontendDir, command: command.Value.(string), args: []string{"run", "build"}})
- case "yarn":
- command := resolveYarnCli()
- if !command.OK {
- return command
- }
- return core.Ok(wailsFrontendBuild{dir: frontendDir, command: command.Value.(string), args: []string{"build"}})
- default:
- command := resolveNpmCli()
- if !command.OK {
- return command
- }
- return core.Ok(wailsFrontendBuild{dir: frontendDir, command: command.Value.(string), args: []string{"run", "build"}})
- }
-}
-
-func detectPackageManager(filesystem storage.Medium, dir string) string {
- if declared := detectDeclaredPackageManager(filesystem, dir); declared != "" {
- return declared
- }
-
- lockFiles := []struct {
- file string
- manager string
- }{
- {"bun.lock", "bun"},
- {"bun.lockb", "bun"},
- {"pnpm-lock.yaml", "pnpm"},
- {"yarn.lock", "yarn"},
- {"package-lock.json", "npm"},
- }
-
- for _, lockFile := range lockFiles {
- if filesystem.IsFile(ax.Join(dir, lockFile.file)) {
- return lockFile.manager
- }
- }
-
- return "npm"
-}
-
-type packageJSONManifest struct {
- PackageManager string `json:"packageManager"`
-}
-
-func detectDeclaredPackageManager(filesystem storage.Medium, dir string) string {
- content := filesystem.Read(ax.Join(dir, "package.json"))
- if !content.OK {
- return ""
- }
-
- var manifest packageJSONManifest
- decoded := ax.JSONUnmarshal([]byte(content.Value.(string)), &manifest)
- if !decoded.OK {
- return ""
- }
-
- return normalisePackageManager(manifest.PackageManager)
-}
-
-func normalisePackageManager(value string) string {
- value = core.Trim(value)
- if value == "" {
- return ""
- }
-
- parts := core.SplitN(value, "@", 2)
- manager := parts[0]
-
- switch manager {
- case "bun", "pnpm", "yarn", "npm":
- return manager
- default:
- return ""
- }
-}
-
-type commandArgs struct {
- command string
- args []string
-}
-
-func resolveDenoBuildCommand(cfg WailsBuildConfig) core.Result {
- override := core.Trim(core.Env("DENO_BUILD"))
- if override == "" {
- override = core.Trim(cfg.DenoBuild)
- }
- if override != "" {
- argsResult := splitCommandLine(override)
- if !argsResult.OK {
- return core.Fail(core.E("build.resolveDenoBuildCommand", "invalid DENO_BUILD command", core.NewError(argsResult.Error())))
- }
- args := argsResult.Value.([]string)
- if len(args) == 0 {
- return core.Fail(core.E("build.resolveDenoBuildCommand", "DENO_BUILD command is empty", nil))
- }
- return core.Ok(commandArgs{command: args[0], args: args[1:]})
- }
-
- command := resolveDenoCli()
- if !command.OK {
- return command
- }
- return core.Ok(commandArgs{command: command.Value.(string), args: []string{"task", "build"}})
-}
-
-func splitCommandLine(command string) core.Result {
- command = core.Trim(command)
- if command == "" {
- return core.Ok([]string(nil))
- }
-
- var (
- args []string
- quote rune
- escape bool
- )
- current := core.NewBuilder()
-
- flush := func() {
- if current.Len() == 0 {
- return
- }
- args = append(args, current.String())
- current.Reset()
- }
-
- for _, r := range command {
- switch {
- case escape:
- current.WriteRune(r)
- escape = false
- case r == '\\' && quote != '\'':
- escape = true
- case quote != 0:
- if r == quote {
- quote = 0
- continue
- }
- current.WriteRune(r)
- case r == '"' || r == '\'':
- quote = r
- case unicode.IsSpace(r):
- flush()
- default:
- current.WriteRune(r)
- }
- }
-
- if escape {
- current.WriteRune('\\')
- }
- if quote != 0 {
- return core.Fail(core.E("build.splitCommandLine", "unterminated quote in command", nil))
- }
-
- flush()
- return core.Ok(args)
-}
-
-// CreateUniversal merges two architecture-specific app bundles into a universal app.
-func CreateUniversal(arm64Path, amd64Path, outputPath string) core.Result {
- if arm64Path == "" || amd64Path == "" || outputPath == "" {
- return core.Fail(core.E("build.CreateUniversal", "arm64, amd64, and output paths are required", nil))
- }
-
- if storage.Local.Exists(outputPath) {
- deleted := storage.Local.DeleteAll(outputPath)
- if !deleted.OK {
- return core.Fail(core.E("build.CreateUniversal", "failed to replace existing output bundle", core.NewError(deleted.Error())))
- }
- }
-
- created := storage.Local.EnsureDir(ax.Dir(outputPath))
- if !created.OK {
- return core.Fail(core.E("build.CreateUniversal", "failed to create universal output directory", core.NewError(created.Error())))
- }
- copied := copyPath(storage.Local, arm64Path, outputPath)
- if !copied.OK {
- return core.Fail(core.E("build.CreateUniversal", "failed to copy arm64 bundle", core.NewError(copied.Error())))
- }
-
- lipoCommandResult := resolveLipoCli()
- if !lipoCommandResult.OK {
- return lipoCommandResult
- }
- lipoCommand := lipoCommandResult.Value.(string)
-
- for _, candidate := range universalMergeCandidates(storage.Local, arm64Path, amd64Path) {
- armCandidate := ax.Join(arm64Path, candidate)
- amdCandidate := ax.Join(amd64Path, candidate)
- outputCandidate := ax.Join(outputPath, candidate)
- output := appleCombinedOutput(context.Background(), "", nil, lipoCommand, "-create", "-output", outputCandidate, armCandidate, amdCandidate)
- if !output.OK {
- return core.Fail(core.E("build.CreateUniversal", "lipo failed for "+candidate+": "+output.Error(), core.NewError(output.Error())))
- }
- }
-
- return core.Ok(nil)
-}
-
-// Sign code-signs an app bundle or Apple artefact.
-func Sign(ctx context.Context, cfg SignConfig) core.Result {
- if cfg.AppPath == "" {
- return core.Fail(core.E("build.Sign", "app_path is required", nil))
- }
- if cfg.Identity == "" {
- return core.Fail(core.E("build.Sign", "signing identity is required", nil))
- }
-
- codesignCommandResult := resolveCodesignCli()
- if !codesignCommandResult.OK {
- return codesignCommandResult
- }
- codesignCommand := codesignCommandResult.Value.(string)
-
- if !storage.Local.IsDir(cfg.AppPath) || !core.HasSuffix(cfg.AppPath, ".app") {
- output := appleCombinedOutput(ctx, "", nil, codesignCommand, codesignArgs(cfg, cfg.AppPath, cfg.Entitlements)...)
- if !output.OK {
- return core.Fail(core.E("build.Sign", "codesign failed for "+cfg.AppPath, core.NewError(output.Error())))
- }
- return core.Ok(nil)
- }
-
- for _, path := range signFrameworkPaths(cfg.AppPath) {
- output := appleCombinedOutput(ctx, "", nil, codesignCommand, codesignArgs(cfg, path, "")...)
- if !output.OK {
- return core.Fail(core.E("build.Sign", "codesign failed for framework "+path+": "+output.Error(), core.NewError(output.Error())))
- }
- }
-
- mainBinary := bundleExecutablePath(cfg.AppPath)
- for _, path := range signHelperBinaryPaths(cfg.AppPath, mainBinary) {
- output := appleCombinedOutput(ctx, "", nil, codesignCommand, codesignArgs(cfg, path, "")...)
- if !output.OK {
- return core.Fail(core.E("build.Sign", "codesign failed for helper binary "+path+": "+output.Error(), core.NewError(output.Error())))
- }
- }
-
- output := appleCombinedOutput(ctx, "", nil, codesignCommand, codesignArgs(cfg, mainBinary, cfg.Entitlements)...)
- if !output.OK {
- return core.Fail(core.E("build.Sign", "codesign failed for main binary "+mainBinary+": "+output.Error(), core.NewError(output.Error())))
- }
-
- output = appleCombinedOutput(ctx, "", nil, codesignCommand, codesignArgs(cfg, cfg.AppPath, cfg.Entitlements)...)
- if !output.OK {
- return core.Fail(core.E("build.Sign", "codesign failed for app bundle "+cfg.AppPath+": "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// Notarise submits a signed app bundle or DMG to Apple and staples the ticket.
-func Notarise(ctx context.Context, cfg NotariseConfig) core.Result {
- if cfg.AppPath == "" {
- return core.Fail(core.E("build.Notarise", "app_path is required", nil))
- }
- if ctx == nil {
- ctx = context.Background()
- }
-
- notariseCtx := ctx
- if _, hasDeadline := ctx.Deadline(); !hasDeadline {
- var cancel context.CancelFunc
- notariseCtx, cancel = context.WithTimeout(ctx, 30*time.Minute)
- defer cancel()
- }
-
- authArgsResult := notariseAuthArgs(cfg)
- if !authArgsResult.OK {
- return authArgsResult
- }
- authArgs := authArgsResult.Value.([]string)
-
- dittoCommandResult := resolveDittocli()
- if !dittoCommandResult.OK {
- return dittoCommandResult
- }
- dittoCommand := dittoCommandResult.Value.(string)
- xcrunCommandResult := resolveXcrunCli()
- if !xcrunCommandResult.OK {
- return xcrunCommandResult
- }
- xcrunCommand := xcrunCommandResult.Value.(string)
-
- tempDirResult := ax.TempDir("core-build-notary-*")
- if !tempDirResult.OK {
- return core.Fail(core.E("build.Notarise", "failed to create notarisation temp directory", core.NewError(tempDirResult.Error())))
- }
- tempDir := tempDirResult.Value.(string)
- defer ax.RemoveAll(tempDir)
-
- zipPath := ax.Join(tempDir, ax.Base(cfg.AppPath)+".zip")
- output := appleCombinedOutput(notariseCtx, "", nil, dittoCommand, "-c", "-k", "--keepParent", cfg.AppPath, zipPath)
- if !output.OK {
- return core.Fail(core.E("build.Notarise", "failed to create notarisation archive: "+output.Error(), core.NewError(output.Error())))
- }
-
- submitArgs := []string{"notarytool", "submit", zipPath, "--wait", "--output-format", "json"}
- submitArgs = append(submitArgs, authArgs...)
- output = appleCombinedOutput(notariseCtx, "", nil, xcrunCommand, submitArgs...)
- outputText := ""
- if output.OK {
- outputText = output.Value.(string)
- }
- if !output.OK {
- outputText = appendNotaryLog(notariseCtx, xcrunCommand, authArgs, output.Error())
- return core.Fail(core.E("build.Notarise", "notarisation failed: "+outputText, core.NewError(output.Error())))
- }
-
- status := parseNotaryStatus(outputText)
- if status != "" && core.Lower(status) != "accepted" {
- outputText = appendNotaryLog(notariseCtx, xcrunCommand, authArgs, outputText)
- return core.Fail(core.E("build.Notarise", "Apple rejected notarisation request with status "+status+": "+outputText, nil))
- }
-
- output = appleCombinedOutput(notariseCtx, "", nil, xcrunCommand, "stapler", "staple", cfg.AppPath)
- if !output.OK {
- return core.Fail(core.E("build.Notarise", "failed to staple notarisation ticket: "+output.Error(), core.NewError(output.Error())))
- }
-
- if core.HasSuffix(cfg.AppPath, ".app") {
- spctlCommandResult := resolveSPCTLCli()
- if !spctlCommandResult.OK {
- return spctlCommandResult
- }
- spctlCommand := spctlCommandResult.Value.(string)
- output = appleCombinedOutput(notariseCtx, "", nil, spctlCommand, "--assess", "--type", "execute", cfg.AppPath)
- if !output.OK {
- return core.Fail(core.E("build.Notarise", "Gatekeeper assessment failed: "+output.Error(), core.NewError(output.Error())))
- }
- }
-
- return core.Ok(nil)
-}
-
-// CreateDMG packages an app bundle into a distributable DMG.
-func CreateDMG(ctx context.Context, cfg DMGConfig) core.Result {
- if cfg.AppPath == "" || cfg.OutputPath == "" {
- return core.Fail(core.E("build.CreateDMG", "app_path and output_path are required", nil))
- }
- if ctx == nil {
- ctx = context.Background()
- }
-
- cfg = normaliseDMGConfig(cfg)
-
- tempDirResult := ax.TempDir("core-build-dmg-*")
- if !tempDirResult.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to create DMG staging directory", core.NewError(tempDirResult.Error())))
- }
- tempDir := tempDirResult.Value.(string)
- defer ax.RemoveAll(tempDir)
-
- stageDir := ax.Join(tempDir, "stage")
- mountDir := ax.Join(tempDir, "mount")
- rwDMGPath := ax.Join(tempDir, "staging.dmg")
- created := storage.Local.EnsureDir(stageDir)
- if !created.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to create DMG stage directory", core.NewError(created.Error())))
- }
-
- appName := ax.Base(cfg.AppPath)
- stageAppPath := ax.Join(stageDir, appName)
- copied := copyPath(storage.Local, cfg.AppPath, stageAppPath)
- if !copied.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to stage app bundle", core.NewError(copied.Error())))
- }
-
- if err := syscall.Symlink("/Applications", ax.Join(stageDir, "Applications")); err != nil {
- return core.Fail(core.E("build.CreateDMG", "failed to create Applications symlink", err))
- }
-
- if cfg.Background != "" {
- backgroundDir := ax.Join(stageDir, ".background")
- backgroundCreated := storage.Local.EnsureDir(backgroundDir)
- if !backgroundCreated.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to create DMG background directory", core.NewError(backgroundCreated.Error())))
- }
- backgroundCopied := copyPath(storage.Local, cfg.Background, ax.Join(backgroundDir, ax.Base(cfg.Background)))
- if !backgroundCopied.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to stage DMG background", core.NewError(backgroundCopied.Error())))
- }
- }
-
- outputCreated := storage.Local.EnsureDir(ax.Dir(cfg.OutputPath))
- if !outputCreated.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to create DMG output directory", core.NewError(outputCreated.Error())))
- }
-
- hdiutilCommandResult := resolveHdiutilCli()
- if !hdiutilCommandResult.OK {
- return hdiutilCommandResult
- }
- hdiutilCommand := hdiutilCommandResult.Value.(string)
- osascriptCommandResult := resolveOsaScriptCli()
- if !osascriptCommandResult.OK {
- return osascriptCommandResult
- }
- osascriptCommand := osascriptCommandResult.Value.(string)
-
- volumeName := firstNonEmpty(cfg.VolumeName, core.TrimSuffix(appName, ".app"))
- createArgs := []string{
- "create",
- "-volname", volumeName,
- "-srcfolder", stageDir,
- "-ov",
- "-format", "UDRW",
- rwDMGPath,
- }
- output := appleCombinedOutput(ctx, "", nil, hdiutilCommand, createArgs...)
- if !output.OK {
- return core.Fail(core.E("build.CreateDMG", "hdiutil failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- mountCreated := storage.Local.EnsureDir(mountDir)
- if !mountCreated.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to create DMG mount directory", core.NewError(mountCreated.Error())))
- }
-
- attached := false
- defer func() {
- if attached {
- detachDMG(context.Background(), hdiutilCommand, mountDir)
- }
- }()
-
- attachArgs := []string{
- "attach",
- "-readwrite",
- "-noverify",
- "-noautoopen",
- "-mountpoint", mountDir,
- rwDMGPath,
- }
- output = appleCombinedOutput(ctx, "", nil, hdiutilCommand, attachArgs...)
- if !output.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to mount staging DMG: "+output.Error(), core.NewError(output.Error())))
- }
- attached = true
-
- scriptPath := ax.Join(tempDir, "layout.applescript")
- script := buildDMGAppleScript(volumeName, appName, cfg)
- written := storage.Local.WriteMode(scriptPath, script, 0o644)
- if !written.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to write DMG layout script", core.NewError(written.Error())))
- }
-
- output = appleCombinedOutput(ctx, "", nil, osascriptCommand, scriptPath)
- if !output.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to configure Finder layout: "+output.Error(), core.NewError(output.Error())))
- }
-
- detached := detachDMG(ctx, hdiutilCommand, mountDir)
- if !detached.OK {
- return detached
- }
- attached = false
-
- convertArgs := []string{
- "convert",
- rwDMGPath,
- "-format", "UDZO",
- "-ov",
- "-o", cfg.OutputPath,
- }
- output = appleCombinedOutput(ctx, "", nil, hdiutilCommand, convertArgs...)
- if !output.OK {
- return core.Fail(core.E("build.CreateDMG", "failed to convert DMG: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func normaliseDMGConfig(cfg DMGConfig) DMGConfig {
- if cfg.IconSize <= 0 {
- cfg.IconSize = defaultDMGIconSize
- }
- if cfg.WindowSize[0] <= 0 || cfg.WindowSize[1] <= 0 {
- cfg.WindowSize = [2]int{defaultDMGWindowWidth, defaultDMGWindowHeight}
- }
- if cfg.VolumeName == "" {
- cfg.VolumeName = core.TrimSuffix(ax.Base(cfg.AppPath), ".app")
- }
- return cfg
-}
-
-func buildDMGAppleScript(volumeName, appName string, cfg DMGConfig) string {
- cfg = normaliseDMGConfig(cfg)
- appX, appY, applicationsX, applicationsY := dmgLayoutPositions(cfg.WindowSize, cfg.IconSize)
-
- backgroundLine := ""
- if cfg.Background != "" {
- backgroundLine = core.Sprintf("\n set background picture of opts to file \".background:%s\"", escapeAppleScriptString(ax.Base(cfg.Background)))
- }
-
- return core.Sprintf(
- "tell application \"Finder\"\n"+
- " tell disk \"%s\"\n"+
- " open\n"+
- " set current view of container window to icon view\n"+
- " set toolbar visible of container window to false\n"+
- " set statusbar visible of container window to false\n"+
- " set bounds of container window to {100, 100, %d, %d}\n"+
- " set opts to the icon view options of container window\n"+
- " set arrangement of opts to not arranged\n"+
- " set icon size of opts to %d%s\n"+
- " set position of item \"%s\" of container window to {%d, %d}\n"+
- " set position of item \"Applications\" of container window to {%d, %d}\n"+
- " update without registering applications\n"+
- " delay 1\n"+
- " close\n"+
- " open\n"+
- " update without registering applications\n"+
- " delay 1\n"+
- " end tell\n"+
- "end tell\n",
- escapeAppleScriptString(volumeName),
- 100+cfg.WindowSize[0],
- 100+cfg.WindowSize[1],
- cfg.IconSize,
- backgroundLine,
- escapeAppleScriptString(appName),
- appX,
- appY,
- applicationsX,
- applicationsY,
- )
-}
-
-func dmgLayoutPositions(windowSize [2]int, iconSize int) (int, int, int, int) {
- width := windowSize[0]
- height := windowSize[1]
- if width <= 0 {
- width = defaultDMGWindowWidth
- }
- if height <= 0 {
- height = defaultDMGWindowHeight
- }
- if iconSize <= 0 {
- iconSize = defaultDMGIconSize
- }
-
- appX := width / 4
- if appX < iconSize+32 {
- appX = iconSize + 32
- }
- applicationsX := (width * 3) / 4
- if applicationsX <= appX {
- applicationsX = appX + iconSize + 96
- }
- appY := height / 2
- if appY < iconSize+32 {
- appY = iconSize + 32
- }
-
- return appX, appY, applicationsX, appY
-}
-
-func escapeAppleScriptString(value string) string {
- return core.Replace(core.Replace(value, `\`, `\\`), `"`, `\"`)
-}
-
-func detachDMG(ctx context.Context, hdiutilCommand, mountDir string) core.Result {
- output := appleCombinedOutput(ctx, "", nil, hdiutilCommand, "detach", mountDir)
- if output.OK {
- return core.Ok(nil)
- }
-
- forceOutput := appleCombinedOutput(ctx, "", nil, hdiutilCommand, "detach", mountDir, "-force")
- if !forceOutput.OK {
- message := output.Error()
- if forceOutput.Error() != "" {
- message = core.Join("\n", output.Error(), forceOutput.Error())
- }
- return core.Fail(core.E("build.CreateDMG", "failed to detach staging DMG: "+message, core.NewError(forceOutput.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// UploadTestFlight uploads a packaged macOS artefact to TestFlight.
-func UploadTestFlight(ctx context.Context, cfg TestFlightConfig) core.Result {
- if cfg.AppPath == "" {
- return core.Fail(core.E("build.UploadTestFlight", "app_path is required", nil))
- }
- valid := validateAppStoreConnectAPIKey(cfg.APIKeyID, cfg.APIKeyIssuerID, cfg.APIKeyPath, "build.UploadTestFlight")
- if !valid.OK {
- return valid
- }
-
- uploadPackage := packageForASCUpload(ctx, cfg.AppPath, cfg.CertIdentity, cfg.APIKeyID, cfg.APIKeyPath)
- if !uploadPackage.OK {
- return uploadPackage
- }
- upload := uploadPackage.Value.(ascUploadPackage)
- uploadPath := upload.path
- env := upload.env
- cleanup := upload.cleanup
- defer cleanup()
-
- xcrunCommandResult := resolveXcrunCli()
- if !xcrunCommandResult.OK {
- return xcrunCommandResult
- }
- xcrunCommand := xcrunCommandResult.Value.(string)
-
- output := appleCombinedOutput(ctx, "", env, xcrunCommand,
- "altool", "--upload-app", "--type", "macos",
- "--file", uploadPath,
- "--apiKey", cfg.APIKeyID,
- "--apiIssuer", cfg.APIKeyIssuerID,
- )
- if !output.OK {
- return core.Fail(core.E("build.UploadTestFlight", "altool upload failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// SubmitAppStore uploads a packaged macOS artefact for App Store Connect review.
-func SubmitAppStore(ctx context.Context, cfg AppStoreConfig) core.Result {
- if cfg.ReleaseType != "" && cfg.ReleaseType != "manual" && cfg.ReleaseType != "automatic" {
- return core.Fail(core.E("build.SubmitAppStore", "release_type must be manual or automatic", nil))
- }
- if cfg.AppPath == "" {
- return core.Fail(core.E("build.SubmitAppStore", "app_path is required", nil))
- }
- valid := validateAppStoreConnectAPIKey(cfg.APIKeyID, cfg.APIKeyIssuerID, cfg.APIKeyPath, "build.SubmitAppStore")
- if !valid.OK {
- return valid
- }
-
- uploadPackage := packageForASCUpload(ctx, cfg.AppPath, cfg.CertIdentity, cfg.APIKeyID, cfg.APIKeyPath)
- if !uploadPackage.OK {
- return uploadPackage
- }
- upload := uploadPackage.Value.(ascUploadPackage)
- uploadPath := upload.path
- env := upload.env
- cleanup := upload.cleanup
- defer cleanup()
-
- xcrunCommandResult := resolveXcrunCli()
- if !xcrunCommandResult.OK {
- return xcrunCommandResult
- }
- xcrunCommand := xcrunCommandResult.Value.(string)
-
- output := appleCombinedOutput(ctx, "", env, xcrunCommand,
- "altool", "--upload-app", "--type", "macos",
- "--file", uploadPath,
- "--apiKey", cfg.APIKeyID,
- "--apiIssuer", cfg.APIKeyIssuerID,
- )
- if !output.OK {
- return core.Fail(core.E("build.SubmitAppStore", "altool upload failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// WriteInfoPlist writes the app bundle Info.plist and returns its path.
-func WriteInfoPlist(filesystem storage.Medium, appPath string, plist InfoPlist) core.Result {
- if filesystem == nil {
- filesystem = storage.Local
- }
-
- plistPath := ax.Join(appPath, "Contents", "Info.plist")
- created := filesystem.EnsureDir(ax.Dir(plistPath))
- if !created.OK {
- return core.Fail(core.E("build.WriteInfoPlist", "failed to create Info.plist directory", core.NewError(created.Error())))
- }
-
- content := encodePlist(plist.Values())
- if !content.OK {
- return content
- }
- written := filesystem.WriteMode(plistPath, content.Value.(string), 0o644)
- if !written.OK {
- return core.Fail(core.E("build.WriteInfoPlist", "failed to write Info.plist", core.NewError(written.Error())))
- }
-
- return core.Ok(plistPath)
-}
-
-// WriteEntitlements writes an entitlements plist file.
-func WriteEntitlements(filesystem storage.Medium, path string, entitlements Entitlements) core.Result {
- if filesystem == nil {
- filesystem = storage.Local
- }
- if path == "" {
- return core.Fail(core.E("build.WriteEntitlements", "entitlements path is required", nil))
- }
-
- created := filesystem.EnsureDir(ax.Dir(path))
- if !created.OK {
- return core.Fail(core.E("build.WriteEntitlements", "failed to create entitlements directory", core.NewError(created.Error())))
- }
-
- content := encodePlist(entitlements.Values())
- if !content.OK {
- return content
- }
- written := filesystem.WriteMode(path, content.Value.(string), 0o644)
- if !written.OK {
- return core.Fail(core.E("build.WriteEntitlements", "failed to write entitlements", core.NewError(written.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// Values converts InfoPlist to plist key/value pairs.
-func (p InfoPlist) Values() map[string]any {
- return map[string]any{
- "CFBundleDisplayName": p.BundleDisplayName,
- "CFBundleExecutable": p.Executable,
- "CFBundleIdentifier": p.BundleID,
- "CFBundleName": p.BundleName,
- "CFBundlePackageType": "APPL",
- "CFBundleShortVersionString": p.BundleVersion,
- "CFBundleVersion": p.BuildNumber,
- "LSApplicationCategoryType": p.Category,
- "LSMinimumSystemVersion": p.MinSystemVersion,
- "NSHighResolutionCapable": p.HighResCapable,
- "NSHumanReadableCopyright": p.Copyright,
- "NSSupportsSecureRestorableState": p.SupportsSecureRestorableState,
- }
-}
-
-// Values converts Entitlements to plist key/value pairs.
-func (e Entitlements) Values() map[string]any {
- return map[string]any{
- "com.apple.security.app-sandbox": e.Sandbox,
- "com.apple.security.cs.allow-dylib-environment-variables": e.DylibEnvVar,
- "com.apple.security.cs.allow-jit": e.JIT,
- "com.apple.security.cs.allow-unsigned-executable-memory": e.HardenedRuntime,
- "com.apple.security.device.metal": e.MetalGPU,
- "com.apple.security.files.downloads.read-write": e.Downloads,
- "com.apple.security.files.user-selected.read-write": e.UserSelectedReadWrite,
- "com.apple.security.network.client": e.NetworkClient,
- "com.apple.security.network.server": e.NetworkServer,
- }
-}
-
-func directDistributionEntitlements() Entitlements {
- return Entitlements{
- Sandbox: false,
- NetworkClient: true,
- NetworkServer: true,
- MetalGPU: true,
- UserSelectedReadWrite: true,
- Downloads: true,
- HardenedRuntime: true,
- JIT: true,
- DylibEnvVar: false,
- }
-}
-
-func appStoreEntitlements() Entitlements {
- return Entitlements{
- Sandbox: true,
- NetworkClient: true,
- NetworkServer: true,
- MetalGPU: true,
- UserSelectedReadWrite: true,
- Downloads: true,
- HardenedRuntime: false,
- JIT: false,
- DylibEnvVar: false,
- }
-}
-
-func resolveAppleBundleName(cfg *Config) string {
- if cfg.Name != "" {
- return cfg.Name
- }
- if cfg.Project.Binary != "" {
- return cfg.Project.Binary
- }
- if cfg.Project.Name != "" {
- return cfg.Project.Name
- }
- return ax.Base(cfg.ProjectDir)
-}
-
-func resolveAppleOutputDir(cfg *Config) string {
- if cfg.OutputDir != "" {
- return cfg.OutputDir
- }
- return ax.Join(cfg.ProjectDir, "dist", "apple")
-}
-
-func normalizeAppleVersion(version string) string {
- version = core.Trim(version)
- version = core.TrimPrefix(version, "v")
- if version == "" {
- return "0.0.1"
- }
- return version
-}
-
-func appleHasVersionLDFlag(ldflags []string) bool {
- for _, flag := range ldflags {
- if core.Contains(flag, "main.version=") || core.Contains(flag, "main.Version=") {
- return true
- }
- }
- return false
-}
-
-func findBuiltAppBundle(projectDir, name string) core.Result {
- for _, candidate := range []string{
- ax.Join(projectDir, "build", "bin", name+".app"),
- ax.Join(projectDir, "dist", name+".app"),
- ax.Join(projectDir, name+".app"),
- } {
- if storage.Local.Exists(candidate) {
- return core.Ok(candidate)
- }
- }
- return core.Fail(core.E("build.findBuiltAppBundle", "Wails build completed but no .app bundle was found for "+name, nil))
-}
-
-func bundleExecutablePath(appPath string) string {
- executableName := core.TrimSuffix(ax.Base(appPath), ".app")
- infoPlistPath := ax.Join(appPath, "Contents", "Info.plist")
- if content := storage.Local.Read(infoPlistPath); content.OK {
- if name := plistStringValue(content.Value.(string), "CFBundleExecutable"); name != "" {
- executableName = name
- }
- }
- return ax.Join(appPath, "Contents", "MacOS", executableName)
-}
-
-func universalMergeCandidates(filesystem storage.Medium, arm64Path, amd64Path string) []string {
- candidates := map[string]struct{}{}
- seedUniversalMergeCandidates(filesystem, arm64Path, amd64Path, "", candidates)
-
- paths := make([]string, 0, len(candidates))
- for path := range candidates {
- paths = append(paths, path)
- }
- sort.Strings(paths)
- return paths
-}
-
-func seedUniversalMergeCandidates(filesystem storage.Medium, arm64Path, amd64Path, relativePath string, candidates map[string]struct{}) {
- currentPath := arm64Path
- if relativePath != "" {
- currentPath = ax.Join(arm64Path, relativePath)
- }
-
- entriesResult := filesystem.List(currentPath)
- if !entriesResult.OK {
- return
- }
- entries := entriesResult.Value.([]fs.DirEntry)
-
- for _, entry := range entries {
- entryRelativePath := entry.Name()
- if relativePath != "" {
- entryRelativePath = ax.Join(relativePath, entry.Name())
- }
-
- armEntryPath := ax.Join(arm64Path, entryRelativePath)
- amdEntryPath := ax.Join(amd64Path, entryRelativePath)
- if entry.IsDir() {
- if filesystem.IsDir(amdEntryPath) {
- seedUniversalMergeCandidates(filesystem, arm64Path, amd64Path, entryRelativePath, candidates)
- }
- continue
- }
-
- if !filesystem.IsFile(amdEntryPath) || !shouldMergeUniversalPath(filesystem, armEntryPath, entryRelativePath) {
- continue
- }
- candidates[entryRelativePath] = struct{}{}
- }
-}
-
-func shouldMergeUniversalPath(filesystem storage.Medium, path, relativePath string) bool {
- info := filesystem.Stat(path)
- if info.OK && info.Value.(fs.FileInfo).Mode()&0o111 != 0 {
- return true
- }
-
- lowerRelativePath := core.Lower(relativePath)
- if core.HasSuffix(lowerRelativePath, ".dylib") || core.HasSuffix(lowerRelativePath, ".so") {
- return true
- }
-
- for currentDir := ax.Dir(relativePath); currentDir != "." && currentDir != "" && currentDir != string(core.PathSeparator); currentDir = ax.Dir(currentDir) {
- base := ax.Base(currentDir)
- if core.HasSuffix(base, ".framework") {
- return ax.Base(relativePath) == core.TrimSuffix(base, ".framework")
- }
- }
-
- return false
-}
-
-func plistStringValue(content, key string) string {
- pattern := core.Sprintf("%s", key)
- parts := core.SplitN(content, pattern, 2)
- if len(parts) != 2 {
- return ""
- }
-
- remainder := parts[1]
- startTag := ""
- endTag := ""
- startParts := core.SplitN(remainder, startTag, 2)
- if len(startParts) != 2 {
- return ""
- }
- endParts := core.SplitN(startParts[1], endTag, 2)
- if len(endParts) != 2 {
- return ""
- }
- return core.Trim(endParts[0])
-}
-
-func copyPath(filesystem storage.Medium, sourcePath, destPath string) core.Result {
- if filesystem == nil {
- filesystem = storage.Local
- }
-
- if filesystem.IsDir(sourcePath) {
- created := filesystem.EnsureDir(destPath)
- if !created.OK {
- return created
- }
- entriesResult := filesystem.List(sourcePath)
- if !entriesResult.OK {
- return entriesResult
- }
- entries := entriesResult.Value.([]fs.DirEntry)
- for _, entry := range entries {
- copied := copyPath(filesystem, ax.Join(sourcePath, entry.Name()), ax.Join(destPath, entry.Name()))
- if !copied.OK {
- return copied
- }
- }
- return core.Ok(nil)
- }
-
- infoResult := filesystem.Stat(sourcePath)
- if !infoResult.OK {
- return infoResult
- }
- info := infoResult.Value.(fs.FileInfo)
- content := filesystem.Read(sourcePath)
- if !content.OK {
- return content
- }
- return filesystem.WriteMode(destPath, content.Value.(string), info.Mode().Perm())
-}
-
-func signFrameworkPaths(appPath string) []string {
- frameworksDir := ax.Join(appPath, "Contents", "Frameworks")
- if !storage.Local.IsDir(frameworksDir) {
- return nil
- }
-
- entriesResult := storage.Local.List(frameworksDir)
- if !entriesResult.OK {
- return nil
- }
- entries := entriesResult.Value.([]fs.DirEntry)
-
- var paths []string
- for _, entry := range entries {
- paths = append(paths, ax.Join(frameworksDir, entry.Name()))
- }
- sort.Strings(paths)
- return paths
-}
-
-func signHelperBinaryPaths(appPath, mainBinary string) []string {
- macOSDir := ax.Join(appPath, "Contents", "MacOS")
- if !storage.Local.IsDir(macOSDir) {
- return nil
- }
-
- entriesResult := storage.Local.List(macOSDir)
- if !entriesResult.OK {
- return nil
- }
- entries := entriesResult.Value.([]fs.DirEntry)
-
- var paths []string
- for _, entry := range entries {
- path := ax.Join(macOSDir, entry.Name())
- if path == mainBinary {
- continue
- }
- if entry.IsDir() {
- continue
- }
- info, err := entry.Info()
- if err != nil {
- continue
- }
- if info.Mode()&0111 == 0 {
- continue
- }
- paths = append(paths, path)
- }
- sort.Strings(paths)
- return paths
-}
-
-func codesignArgs(cfg SignConfig, path string, entitlements string) []string {
- args := []string{
- "--sign", cfg.Identity,
- "--timestamp",
- "--force",
- }
- if cfg.KeychainPath != "" {
- args = append(args, "--keychain", cfg.KeychainPath)
- }
- if cfg.Hardened {
- args = append(args, "--options", "runtime")
- }
- if cfg.Deep {
- args = append(args, "--deep")
- }
- if entitlements != "" {
- args = append(args, "--entitlements", entitlements)
- }
- args = append(args, path)
- return args
-}
-
-func notariseAuthArgs(cfg NotariseConfig) core.Result {
- if cfg.APIKeyID != "" {
- if cfg.APIKeyIssuerID == "" || cfg.APIKeyPath == "" {
- return core.Fail(core.E("build.notariseAuthArgs", "api_key_issuer_id and api_key_path are required with api_key_id", nil))
- }
- return core.Ok([]string{
- "--key", cfg.APIKeyPath,
- "--key-id", cfg.APIKeyID,
- "--issuer", cfg.APIKeyIssuerID,
- })
- }
-
- if cfg.AppleID == "" || cfg.Password == "" || cfg.TeamID == "" {
- return core.Fail(core.E("build.notariseAuthArgs", "team_id, apple_id, and password are required when API key auth is not configured", nil))
- }
-
- return core.Ok([]string{
- "--apple-id", cfg.AppleID,
- "--password", cfg.Password,
- "--team-id", cfg.TeamID,
- })
-}
-
-func validateAppStoreConnectAPIKey(apiKeyID, apiKeyIssuerID, apiKeyPath, op string) core.Result {
- switch {
- case core.Trim(apiKeyID) == "":
- return core.Fail(core.E(op, "api_key_id is required for App Store Connect uploads", nil))
- case core.Trim(apiKeyIssuerID) == "":
- return core.Fail(core.E(op, "api_key_issuer_id is required for App Store Connect uploads", nil))
- case core.Trim(apiKeyPath) == "":
- return core.Fail(core.E(op, "api_key_path is required for App Store Connect uploads", nil))
- default:
- return core.Ok(nil)
- }
-}
-
-func isDeveloperIDIdentity(identity string) bool {
- return core.Contains(core.Lower(identity), "developer id")
-}
-
-func validateAppStorePreflight(filesystem storage.Medium, projectDir, bundlePath string, options AppleOptions) core.Result {
- if filesystem == nil {
- filesystem = storage.Local
- }
-
- metadata := validateAppStoreMetadata(filesystem, projectDir, options.MetadataPath)
- if !metadata.OK {
- return metadata
- }
- scanned := scanBundleForPrivateAPIUsage(filesystem, bundlePath)
- if !scanned.OK {
- return scanned
- }
-
- return core.Ok(nil)
-}
-
-func validateAppStoreMetadata(filesystem storage.Medium, projectDir, configuredPath string) core.Result {
- metadataPath := resolveAppStoreMetadataPath(filesystem, projectDir, configuredPath)
- if metadataPath == "" {
- return core.Fail(core.E("build.validateAppStoreMetadata", "App Store submissions require metadata_path or a standard metadata directory (.core/apple/appstore, .core/appstore, or appstore)", nil))
- }
-
- if !hasAppStoreDescription(filesystem, metadataPath) {
- return core.Fail(core.E("build.validateAppStoreMetadata", "App Store submissions require a description file in metadata_path", nil))
- }
- if !hasAppStoreScreenshots(filesystem, metadataPath) {
- return core.Fail(core.E("build.validateAppStoreMetadata", "App Store submissions require at least one screenshot in metadata_path/screenshots", nil))
- }
-
- return core.Ok(nil)
-}
-
-func resolveAppStoreMetadataPath(filesystem storage.Medium, projectDir, configuredPath string) string {
- candidates := []string{}
- if configuredPath != "" {
- if ax.IsAbs(configuredPath) {
- candidates = append(candidates, configuredPath)
- } else {
- candidates = append(candidates, ax.Join(projectDir, configuredPath))
- }
- }
- candidates = append(candidates,
- ax.Join(projectDir, ".core", "apple", "appstore"),
- ax.Join(projectDir, ".core", "appstore"),
- ax.Join(projectDir, "appstore"),
- )
-
- for _, candidate := range candidates {
- if candidate != "" && filesystem.IsDir(candidate) {
- return candidate
- }
- }
-
- return ""
-}
-
-func hasAppStoreDescription(filesystem storage.Medium, metadataPath string) bool {
- for _, name := range []string{"description.txt", "description.md", "description.markdown"} {
- if filesystem.IsFile(ax.Join(metadataPath, name)) {
- return true
- }
- }
- return false
-}
-
-func hasAppStoreScreenshots(filesystem storage.Medium, metadataPath string) bool {
- screenshotsDir := ax.Join(metadataPath, "screenshots")
- if !filesystem.IsDir(screenshotsDir) {
- return false
- }
-
- entriesResult := filesystem.List(screenshotsDir)
- if !entriesResult.OK {
- return false
- }
- entries := entriesResult.Value.([]fs.DirEntry)
-
- for _, entry := range entries {
- if entry.IsDir() {
- continue
- }
-
- name := core.Lower(entry.Name())
- if core.HasSuffix(name, ".png") ||
- core.HasSuffix(name, ".jpg") ||
- core.HasSuffix(name, ".jpeg") ||
- core.HasSuffix(name, ".heic") {
- return true
- }
- }
-
- return false
-}
-
-func validatePrivacyPolicyURL(raw string) core.Result {
- value := core.Trim(raw)
- if value == "" {
- return core.Fail(core.E("build.validatePrivacyPolicyURL", "App Store submissions require privacy_policy_url (for example https://lthn.ai/privacy)", nil))
- }
-
- normalised := value
- if !core.Contains(normalised, "://") {
- normalised = "https://" + normalised
- }
-
- parsed, err := url.Parse(normalised)
- if err != nil {
- return core.Fail(core.E("build.validatePrivacyPolicyURL", "privacy_policy_url must be a valid URL", err))
- }
- if core.Trim(parsed.Host) == "" || parsed.Path == "" || parsed.Path == "/" {
- return core.Fail(core.E("build.validatePrivacyPolicyURL", "privacy_policy_url must include a host and non-root path", nil))
- }
-
- return core.Ok(nil)
-}
-
-func scanBundleForPrivateAPIUsage(filesystem storage.Medium, bundlePath string) core.Result {
- if bundlePath == "" {
- return core.Fail(core.E("build.scanBundleForPrivateAPIUsage", "bundle path is required", nil))
- }
-
- for _, root := range privateAPIScanRoots(bundlePath) {
- for _, path := range collectBundleFiles(filesystem, root) {
- content := filesystem.Read(path)
- if !content.OK {
- continue
- }
- if indicator := detectPrivateAPIIndicator(content.Value.(string)); indicator != "" {
- return core.Fail(core.E("build.scanBundleForPrivateAPIUsage", "private API usage detected in "+path+": "+indicator, nil))
- }
- }
- }
-
- return core.Ok(nil)
-}
-
-func privateAPIScanRoots(bundlePath string) []string {
- return []string{
- ax.Join(bundlePath, "Contents", "MacOS"),
- ax.Join(bundlePath, "Contents", "Frameworks"),
- }
-}
-
-func collectBundleFiles(filesystem storage.Medium, root string) []string {
- if filesystem == nil || !filesystem.Exists(root) {
- return nil
- }
- if !filesystem.IsDir(root) {
- return []string{root}
- }
-
- entriesResult := filesystem.List(root)
- if !entriesResult.OK {
- return nil
- }
- entries := entriesResult.Value.([]fs.DirEntry)
-
- var paths []string
- for _, entry := range entries {
- path := ax.Join(root, entry.Name())
- if entry.IsDir() {
- paths = append(paths, collectBundleFiles(filesystem, path)...)
- continue
- }
- paths = append(paths, path)
- }
-
- return paths
-}
-
-func detectPrivateAPIIndicator(content string) string {
- for _, indicator := range []string{
- "/System/Library/PrivateFrameworks/",
- "PrivateFrameworks/",
- "com.apple.private.",
- "LSApplicationWorkspace",
- "MobileInstallation",
- "SpringBoardServices",
- } {
- if core.Contains(content, indicator) {
- return indicator
- }
- }
-
- return ""
-}
-
-func compareAppleVersion(left, right string) int {
- leftParts := appleVersionParts(left)
- rightParts := appleVersionParts(right)
-
- maxLen := len(leftParts)
- if len(rightParts) > maxLen {
- maxLen = len(rightParts)
- }
-
- for i := 0; i < maxLen; i++ {
- var leftValue, rightValue int
- if i < len(leftParts) {
- leftValue = leftParts[i]
- }
- if i < len(rightParts) {
- rightValue = rightParts[i]
- }
- switch {
- case leftValue < rightValue:
- return -1
- case leftValue > rightValue:
- return 1
- }
- }
-
- return 0
-}
-
-func appleVersionParts(value string) []int {
- value = core.Trim(core.TrimPrefix(value, "v"))
- if value == "" {
- return nil
- }
-
- rawParts := core.Split(value, ".")
- parts := make([]int, 0, len(rawParts))
- for _, rawPart := range rawParts {
- part := core.Trim(rawPart)
- if part == "" {
- parts = append(parts, 0)
- continue
- }
-
- digits := core.NewBuilder()
- for _, r := range part {
- if r < '0' || r > '9' {
- break
- }
- digits.WriteRune(r)
- }
-
- if digits.Len() == 0 {
- parts = append(parts, 0)
- continue
- }
-
- number, err := strconv.Atoi(digits.String())
- if err != nil {
- parts = append(parts, 0)
- continue
- }
- parts = append(parts, number)
- }
-
- return parts
-}
-
-func extractNotaryRequestID(output string) string {
- if output == "" {
- return ""
- }
-
- var payload struct {
- ID string `json:"id"`
- }
- if decoded := core.JSONUnmarshal([]byte(output), &payload); decoded.OK {
- return payload.ID
- }
- return ""
-}
-
-func parseNotaryStatus(output string) string {
- if output == "" {
- return ""
- }
-
- var payload struct {
- Status string `json:"status"`
- }
- if decoded := core.JSONUnmarshal([]byte(output), &payload); decoded.OK {
- return payload.Status
- }
- return ""
-}
-
-func appendNotaryLog(ctx context.Context, xcrunCommand string, authArgs []string, output string) string {
- requestID := extractNotaryRequestID(output)
- if requestID == "" {
- return output
- }
-
- logArgs := []string{"notarytool", notaryToolLogCommand, requestID}
- logArgs = append(logArgs, authArgs...)
- logOutput := appleCombinedOutput(ctx, "", nil, xcrunCommand, logArgs...)
- if !logOutput.OK || logOutput.Value.(string) == "" {
- return output
- }
-
- return core.Join("\n", output, logOutput.Value.(string))
-}
-
-type ascUploadPackage struct {
- path string
- env []string
- cleanup func()
-}
-
-func packageForASCUpload(ctx context.Context, appPath, certIdentity, apiKeyID, apiKeyPath string) core.Result {
- if core.HasSuffix(appPath, ".pkg") {
- envResult := prepareASCAPIKeyEnv(apiKeyID, apiKeyPath)
- if !envResult.OK {
- return envResult
- }
- env := envResult.Value.(ascAPIKeyEnv)
- return core.Ok(ascUploadPackage{path: appPath, env: env.env, cleanup: env.cleanup})
- }
-
- if !core.HasSuffix(appPath, ".app") {
- return core.Fail(core.E("build.packageForASCUpload", "App Store Connect uploads require a .app or .pkg input", nil))
- }
-
- outputPath := ax.Join(ax.Dir(appPath), core.TrimSuffix(ax.Base(appPath), ".app")+".pkg")
- created := createDistributionPackage(ctx, appPath, certIdentity, outputPath)
- if !created.OK {
- return created
- }
-
- envResult := prepareASCAPIKeyEnv(apiKeyID, apiKeyPath)
- if !envResult.OK {
- return envResult
- }
- env := envResult.Value.(ascAPIKeyEnv)
-
- return core.Ok(ascUploadPackage{path: outputPath, env: env.env, cleanup: env.cleanup})
-}
-
-type ascAPIKeyEnv struct {
- env []string
- cleanup func()
-}
-
-func prepareASCAPIKeyEnv(apiKeyID, apiKeyPath string) core.Result {
- if apiKeyPath == "" {
- return core.Ok(ascAPIKeyEnv{cleanup: func() {}})
- }
-
- expectedName := core.Sprintf("AuthKey_%s.p8", apiKeyID)
- if expectedName == "AuthKey_.p8" || ax.Base(apiKeyPath) == expectedName {
- return core.Ok(ascAPIKeyEnv{env: []string{"API_PRIVATE_KEYS_DIR=" + ax.Dir(apiKeyPath)}, cleanup: func() {}})
- }
-
- content := storage.Local.Read(apiKeyPath)
- if !content.OK {
- return core.Fail(core.E("build.prepareASCAPIKeyEnv", "failed to read App Store Connect API key", core.NewError(content.Error())))
- }
-
- tempDirResult := ax.TempDir("core-build-asc-key-*")
- if !tempDirResult.OK {
- return core.Fail(core.E("build.prepareASCAPIKeyEnv", "failed to create App Store Connect key staging directory", core.NewError(tempDirResult.Error())))
- }
- tempDir := tempDirResult.Value.(string)
-
- stagedPath := ax.Join(tempDir, expectedName)
- written := storage.Local.WriteMode(stagedPath, content.Value.(string), 0o600)
- if !written.OK {
- cleaned := ax.RemoveAll(tempDir)
- if !cleaned.OK {
- return core.Fail(core.E("build.prepareASCAPIKeyEnv", "failed to clean up App Store Connect key staging directory", core.NewError(cleaned.Error())))
- }
- return core.Fail(core.E("build.prepareASCAPIKeyEnv", "failed to stage App Store Connect API key", core.NewError(written.Error())))
- }
-
- return core.Ok(ascAPIKeyEnv{
- env: []string{"API_PRIVATE_KEYS_DIR=" + tempDir},
- cleanup: func() {
- ax.RemoveAll(tempDir)
- },
- })
-}
-
-func createDistributionPackage(ctx context.Context, appPath, certIdentity, outputPath string) core.Result {
- productbuildCommandResult := resolveProductbuildCli()
- if !productbuildCommandResult.OK {
- return productbuildCommandResult
- }
- productbuildCommand := productbuildCommandResult.Value.(string)
-
- args := []string{"--component", appPath, "/Applications", outputPath}
- if certIdentity != "" {
- args = append([]string{"--sign", certIdentity}, args...)
- }
-
- output := appleCombinedOutput(ctx, "", nil, productbuildCommand, args...)
- if !output.OK {
- return core.Fail(core.E("build.createDistributionPackage", "productbuild failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func encodePlist(values map[string]any) core.Result {
- keys := make([]string, 0, len(values))
- for key := range values {
- keys = append(keys, key)
- }
- sort.Strings(keys)
-
- buf := core.NewBuffer()
- buf.WriteString(xml.Header)
- buf.WriteString(``)
- buf.WriteString(``)
-
- for _, key := range keys {
- buf.WriteString("")
- if err := xml.EscapeText(buf, []byte(key)); err != nil {
- return core.Fail(core.E("build.encodePlist", "failed to encode plist key", err))
- }
- buf.WriteString("")
-
- switch value := values[key].(type) {
- case string:
- buf.WriteString("")
- if err := xml.EscapeText(buf, []byte(value)); err != nil {
- return core.Fail(core.E("build.encodePlist", "failed to encode plist string value", err))
- }
- buf.WriteString("")
- case bool:
- if value {
- buf.WriteString("")
- } else {
- buf.WriteString("")
- }
- case int:
- buf.WriteString("")
- buf.WriteString(strconv.Itoa(value))
- buf.WriteString("")
- default:
- return core.Fail(core.E("build.encodePlist", "unsupported plist value type", nil))
- }
- }
-
- buf.WriteString("")
- return core.Ok(buf.String())
-}
-
-func appendEnvIfMissing(env []string, key, value string) []string {
- prefix := key + "="
- for _, entry := range env {
- if core.HasPrefix(entry, prefix) {
- return env
- }
- }
- return append(env, prefix+value)
-}
-
-func resolveWails3Cli() core.Result {
- paths := []string{
- "/usr/local/bin/wails3",
- "/opt/homebrew/bin/wails3",
- }
- if home := core.Env("HOME"); home != "" {
- paths = append(paths, ax.Join(home, "go", "bin", "wails3"))
- }
- command := appleResolveCommand("wails3", paths...)
- if command.OK {
- return command
- }
-
- fallbacks := []string{
- "/usr/local/bin/wails",
- "/opt/homebrew/bin/wails",
- }
- if home := core.Env("HOME"); home != "" {
- fallbacks = append(fallbacks, ax.Join(home, "go", "bin", "wails"))
- }
- fallback := appleResolveCommand("wails", fallbacks...)
- if !fallback.OK {
- return core.Fail(core.E("build.resolveWails3Cli", "wails3 CLI not found. Install Wails v3 or expose it on PATH.", core.NewError(command.Error())))
- }
- return fallback
-}
-
-func resolveDenoCli() core.Result {
- command := appleResolveCommand("deno", "/usr/local/bin/deno", "/opt/homebrew/bin/deno")
- if !command.OK {
- return core.Fail(core.E("build.resolveDenoCli", "deno CLI not found. Install it from https://deno.com/runtime", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveNpmCli() core.Result {
- command := appleResolveCommand("npm", "/usr/local/bin/npm", "/opt/homebrew/bin/npm")
- if !command.OK {
- return core.Fail(core.E("build.resolveNpmCli", "npm CLI not found. Install Node.js from https://nodejs.org/", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveBunCli() core.Result {
- command := appleResolveCommand("bun", "/usr/local/bin/bun", "/opt/homebrew/bin/bun")
- if !command.OK {
- return core.Fail(core.E("build.resolveBunCli", "bun CLI not found. Install it from https://bun.sh/", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolvePnpmCli() core.Result {
- command := appleResolveCommand("pnpm", "/usr/local/bin/pnpm", "/opt/homebrew/bin/pnpm")
- if !command.OK {
- return core.Fail(core.E("build.resolvePnpmCli", "pnpm CLI not found. Install it from https://pnpm.io/installation", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveYarnCli() core.Result {
- command := appleResolveCommand("yarn", "/usr/local/bin/yarn", "/opt/homebrew/bin/yarn")
- if !command.OK {
- return core.Fail(core.E("build.resolveYarnCli", "yarn CLI not found. Install it from https://yarnpkg.com/getting-started/install", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveLipoCli() core.Result {
- command := appleResolveCommand("lipo", "/usr/bin/lipo", "/usr/local/bin/lipo", "/opt/homebrew/bin/lipo")
- if !command.OK {
- return core.Fail(core.E("build.resolveLipoCli", "lipo not found. Install Xcode Command Line Tools.", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveCodesignCli() core.Result {
- command := appleResolveCommand("codesign", "/usr/bin/codesign", "/usr/local/bin/codesign", "/opt/homebrew/bin/codesign")
- if !command.OK {
- return core.Fail(core.E("build.resolveCodesignCli", "codesign not found. Install Xcode Command Line Tools.", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveDittocli() core.Result {
- command := appleResolveCommand("ditto", "/usr/bin/ditto", "/usr/local/bin/ditto", "/opt/homebrew/bin/ditto")
- if !command.OK {
- return core.Fail(core.E("build.resolveDittocli", "ditto not found. Install Xcode Command Line Tools.", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveXcrunCli() core.Result {
- command := appleResolveCommand("xcrun", "/usr/bin/xcrun", "/usr/local/bin/xcrun", "/opt/homebrew/bin/xcrun")
- if !command.OK {
- return core.Fail(core.E("build.resolveXcrunCli", "xcrun not found. Install Xcode Command Line Tools.", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveSPCTLCli() core.Result {
- command := appleResolveCommand("spctl", "/usr/sbin/spctl", "/usr/local/bin/spctl", "/opt/homebrew/bin/spctl")
- if !command.OK {
- return core.Fail(core.E("build.resolveSPCTLCli", "spctl not found on this system.", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveHdiutilCli() core.Result {
- command := appleResolveCommand("hdiutil", "/usr/bin/hdiutil", "/usr/local/bin/hdiutil", "/opt/homebrew/bin/hdiutil")
- if !command.OK {
- return core.Fail(core.E("build.resolveHdiutilCli", "hdiutil not found. macOS disk image tools are required.", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveOsaScriptCli() core.Result {
- command := appleResolveCommand("osascript", "/usr/bin/osascript", "/usr/local/bin/osascript", "/opt/homebrew/bin/osascript")
- if !command.OK {
- return core.Fail(core.E("build.resolveOsaScriptCli", "osascript not found. Finder automation is required for DMG layout.", core.NewError(command.Error())))
- }
- return command
-}
-
-func resolveProductbuildCli() core.Result {
- command := appleResolveCommand("productbuild", "/usr/bin/productbuild", "/usr/local/bin/productbuild", "/opt/homebrew/bin/productbuild")
- if !command.OK {
- return core.Fail(core.E("build.resolveProductbuildCli", "productbuild not found. Install Xcode Command Line Tools.", core.NewError(command.Error())))
- }
- return command
-}
diff --git a/pkg/build/apple/apple.go b/pkg/build/apple/apple.go
deleted file mode 100644
index 6bf09d3..0000000
--- a/pkg/build/apple/apple.go
+++ /dev/null
@@ -1,589 +0,0 @@
-package apple
-
-import (
- "context"
- "regexp"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- build "dappco.re/go/build/pkg/build"
- "dappco.re/go/build/pkg/release"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-// AppleOptions aliases the core Apple pipeline options.
-type AppleOptions = build.AppleOptions
-
-// WailsBuildConfig mirrors the RFC-facing Apple wrapper input shape.
-// The wrapper keeps LDFlags as a single string while the lower-level build
-// package accepts a slice for direct CLI assembly.
-type WailsBuildConfig struct {
- ProjectDir string `json:"project_dir" yaml:"project_dir"`
- Name string `json:"name" yaml:"name"`
- Arch string `json:"arch" yaml:"arch"`
- BuildTags []string `json:"build_tags" yaml:"build_tags"`
- LDFlags string `json:"ldflags" yaml:"ldflags"`
- OutputDir string `json:"output_dir" yaml:"output_dir"`
- Version string `json:"version" yaml:"version"`
- Env []string `json:"env" yaml:"env"`
- DenoBuild string `json:"deno_build" yaml:"deno_build"`
-}
-
-// SignConfig aliases the codesign configuration.
-type SignConfig = build.SignConfig
-
-// NotariseConfig aliases the notarisation configuration.
-type NotariseConfig = build.NotariseConfig
-
-// DMGConfig aliases the DMG packaging configuration.
-type DMGConfig = build.DMGConfig
-
-// TestFlightConfig aliases the TestFlight upload configuration.
-type TestFlightConfig = build.TestFlightConfig
-
-// AppStoreConfig aliases the App Store Connect submission configuration.
-type AppStoreConfig = build.AppStoreConfig
-
-// InfoPlist aliases the generated Info.plist model.
-type InfoPlist = build.InfoPlist
-
-// Entitlements aliases the generated entitlements model.
-type Entitlements = build.Entitlements
-
-// XcodeCloudConfig aliases the Xcode Cloud workflow metadata stored in build config.
-type XcodeCloudConfig = build.XcodeCloudConfig
-
-// XcodeCloudTrigger aliases a single Xcode Cloud trigger rule.
-type XcodeCloudTrigger = build.XcodeCloudTrigger
-
-// Builder defines the RFC-facing Apple builder contract.
-type Builder interface {
- Name() string
- Detect(fs coreio.Medium, dir string) core.Result
- Build(ctx context.Context, cfg *AppleOptions) core.Result
-}
-
-// AppleBuilder wraps the existing Apple pipeline with functional options.
-type AppleBuilder struct {
- *core.ServiceRuntime[AppleOptions]
- options AppleOptions
- explicit explicitOptions
-}
-
-type explicitOptions struct {
- arch bool
- sign bool
- notarise bool
- dmg bool
- testFlight bool
- appStore bool
-}
-
-// Option configures Apple pipeline defaults for a new AppleBuilder.
-type Option func(*AppleOptions)
-
-var (
- loadConfigFn = build.LoadConfig
- buildAppleFn = build.BuildApple
- determineVersion = release.DetermineVersionWithContext
- getwdFn = ax.Getwd
- runDirFn = ax.RunDir
- buildWailsAppFn = build.BuildWailsApp
- createUniversalFn = build.CreateUniversal
- signFn = build.Sign
- notariseFn = build.Notarise
- createDMGFn = build.CreateDMG
- uploadTFn = build.UploadTestFlight
- submitASFn = build.SubmitAppStore
- writeXcodeCloudScriptsFn = build.WriteXcodeCloudScripts
-)
-
-// Register wires AppleBuilder into the Core service container and seeds the
-// builders registry when the host Core exposes one.
-func Register(c *core.Core) core.Result {
- if c == nil {
- return core.Fail(core.E("apple.Register", "core is nil", nil))
- }
-
- builder := New()
- builder.ServiceRuntime = core.NewServiceRuntime[AppleOptions](c, builder.options)
- if r := c.RegistryOf("builders").Set("apple", builder); !r.OK {
- return r
- }
- if r := c.RegisterService("apple", builder); !r.OK {
- return r
- }
-
- return core.Ok(builder)
-}
-
-// New constructs an AppleBuilder with functional options.
-func New(opts ...Option) *AppleBuilder {
- builder := &AppleBuilder{
- options: build.DefaultAppleOptions(),
- }
- for _, opt := range opts {
- builder.applyOption(opt)
- }
- builder.ServiceRuntime = core.NewServiceRuntime[AppleOptions](nil, builder.options)
- return builder
-}
-
-// WithArch sets the target architecture.
-func WithArch(arch string) Option {
- return func(options *AppleOptions) {
- if options == nil {
- return
- }
- options.Arch = arch
- }
-}
-
-// WithSign enables or disables code signing.
-func WithSign(sign bool) Option {
- return func(options *AppleOptions) {
- if options == nil {
- return
- }
- options.Sign = sign
- }
-}
-
-// WithNotarise enables or disables notarisation.
-func WithNotarise(notarise bool) Option {
- return func(options *AppleOptions) {
- if options == nil {
- return
- }
- options.Notarise = notarise
- }
-}
-
-// WithDMG enables or disables DMG creation.
-func WithDMG(dmg bool) Option {
- return func(options *AppleOptions) {
- if options == nil {
- return
- }
- options.DMG = dmg
- }
-}
-
-// WithTestFlight enables or disables TestFlight upload.
-func WithTestFlight(tf bool) Option {
- return func(options *AppleOptions) {
- if options == nil {
- return
- }
- options.TestFlight = tf
- }
-}
-
-// WithAppStore enables or disables App Store submission.
-func WithAppStore(appStore bool) Option {
- return func(options *AppleOptions) {
- if options == nil {
- return
- }
- options.AppStore = appStore
- }
-}
-
-// Name returns the builder identifier.
-func (b *AppleBuilder) Name() string {
- return "apple"
-}
-
-// Detect reports whether the current directory looks like a Wails-backed Apple target.
-func (b *AppleBuilder) Detect(fs coreio.Medium, dir string) core.Result {
- if fs == nil {
- fs = coreio.Local
- }
- return core.Ok(build.IsWailsProject(fs, dir))
-}
-
-// Build runs the Apple pipeline for the current working directory and returns the .app bundle path.
-func (b *AppleBuilder) Build(ctx context.Context, cfg *AppleOptions) core.Result {
- if ctx == nil {
- ctx = context.Background()
- }
-
- projectDirResult := getwdFn()
- if !projectDirResult.OK {
- return projectDirResult
- }
- projectDir := projectDirResult.Value.(string)
-
- buildConfigResult := loadConfigFn(coreio.Local, projectDir)
- if !buildConfigResult.OK {
- return buildConfigResult
- }
- buildConfig := buildConfigResult.Value.(*build.BuildConfig)
- cacheSetup := build.SetupBuildCache(coreio.Local, projectDir, buildConfig)
- if !cacheSetup.OK {
- return cacheSetup
- }
- if build.HasXcodeCloudConfig(buildConfig) {
- written := writeXcodeCloudScriptsFn(coreio.Local, projectDir, buildConfig)
- if !written.OK {
- return written
- }
- }
-
- versionResult := determineVersion(ctx, projectDir)
- if !versionResult.OK {
- return versionResult
- }
- version := versionResult.Value.(string)
-
- buildNumberResult := resolveBuildNumber(ctx, projectDir)
- if !buildNumberResult.OK {
- return buildNumberResult
- }
- buildNumber := buildNumberResult.Value.(string)
-
- options := b.resolveOptions(buildConfig, cfg)
- name := resolveBundleName(buildConfig, projectDir)
- outputDir := ax.Join(projectDir, "dist", "apple")
- runtimeCfg := runtimeConfig(coreio.Local, projectDir, outputDir, name, buildConfig, version)
-
- result := buildAppleFn(ctx, runtimeCfg, options, buildNumber)
- if !result.OK {
- return result
- }
- buildResult := result.Value.(*build.AppleBuildResult)
-
- return core.Ok(buildResult.BundlePath)
-}
-
-// BuildWailsApp compiles the Wails application for a single Apple architecture.
-func BuildWailsApp(ctx context.Context, cfg WailsBuildConfig) core.Result {
- projectDir := cfg.ProjectDir
- if projectDir == "" {
- projectDirResult := getwdFn()
- if !projectDirResult.OK {
- return projectDirResult
- }
- projectDir = projectDirResult.Value.(string)
- }
-
- buildCfg := build.WailsBuildConfig{
- ProjectDir: projectDir,
- Name: cfg.Name,
- Arch: cfg.Arch,
- BuildTags: append([]string{}, cfg.BuildTags...),
- OutputDir: cfg.OutputDir,
- Version: cfg.Version,
- Env: append([]string{}, cfg.Env...),
- DenoBuild: cfg.DenoBuild,
- }
- if core.Trim(cfg.LDFlags) != "" {
- buildCfg.LDFlags = []string{cfg.LDFlags}
- }
-
- return buildWailsAppFn(ctx, buildCfg)
-}
-
-// CreateUniversal merges arm64 and amd64 bundles into a universal bundle.
-func CreateUniversal(arm64Path, amd64Path, outputPath string) core.Result {
- result := createUniversalFn(arm64Path, amd64Path, outputPath)
- if !result.OK {
- return result
- }
- return core.Ok(outputPath)
-}
-
-// Sign code-signs the given Apple artefact.
-func Sign(ctx context.Context, cfg SignConfig) core.Result {
- result := signFn(ctx, cfg)
- if !result.OK {
- return result
- }
- return core.Ok(cfg.AppPath)
-}
-
-// Notarise submits the artefact for Apple notarisation.
-func Notarise(ctx context.Context, cfg NotariseConfig) core.Result {
- result := notariseFn(ctx, cfg)
- if !result.OK {
- return result
- }
- return core.Ok(cfg.AppPath)
-}
-
-// CreateDMG packages the app bundle into a DMG and returns the DMG path.
-func CreateDMG(ctx context.Context, cfg DMGConfig) core.Result {
- result := createDMGFn(ctx, cfg)
- if !result.OK {
- return result
- }
- return core.Ok(cfg.OutputPath)
-}
-
-// UploadTestFlight uploads the packaged build to TestFlight.
-func UploadTestFlight(ctx context.Context, cfg TestFlightConfig) core.Result {
- result := uploadTFn(ctx, cfg)
- if !result.OK {
- return result
- }
- return core.Ok(cfg.AppPath)
-}
-
-// SubmitAppStore uploads the packaged build to App Store Connect.
-func SubmitAppStore(ctx context.Context, cfg AppStoreConfig) core.Result {
- result := submitASFn(ctx, cfg)
- if !result.OK {
- return result
- }
- return core.Ok(cfg.AppPath)
-}
-
-func (b *AppleBuilder) applyOption(opt Option) {
- if b == nil || opt == nil {
- return
- }
-
- var zeroBefore AppleOptions
- zeroAfter := zeroBefore
- opt(&zeroAfter)
-
- defaultBefore := build.DefaultAppleOptions()
- defaultAfter := defaultBefore
- opt(&defaultAfter)
-
- if zeroAfter.Arch != zeroBefore.Arch || defaultAfter.Arch != defaultBefore.Arch {
- b.explicit.arch = true
- }
- if zeroAfter.Sign != zeroBefore.Sign || defaultAfter.Sign != defaultBefore.Sign {
- b.explicit.sign = true
- }
- if zeroAfter.Notarise != zeroBefore.Notarise || defaultAfter.Notarise != defaultBefore.Notarise {
- b.explicit.notarise = true
- }
- if zeroAfter.DMG != zeroBefore.DMG || defaultAfter.DMG != defaultBefore.DMG {
- b.explicit.dmg = true
- }
- if zeroAfter.TestFlight != zeroBefore.TestFlight || defaultAfter.TestFlight != defaultBefore.TestFlight {
- b.explicit.testFlight = true
- }
- if zeroAfter.AppStore != zeroBefore.AppStore || defaultAfter.AppStore != defaultBefore.AppStore {
- b.explicit.appStore = true
- }
-
- opt(&b.options)
-}
-
-func (b *AppleBuilder) resolveOptions(buildConfig *build.BuildConfig, runtime *AppleOptions) AppleOptions {
- options := build.DefaultAppleOptions()
- if buildConfig != nil {
- options = buildConfig.Apple.Resolve()
- options.CertIdentity = firstNonEmpty(options.CertIdentity, buildConfig.Sign.MacOS.Identity)
- options.TeamID = firstNonEmpty(options.TeamID, buildConfig.Sign.MacOS.TeamID)
- options.AppleID = firstNonEmpty(options.AppleID, buildConfig.Sign.MacOS.AppleID)
- options.Password = firstNonEmpty(options.Password, buildConfig.Sign.MacOS.AppPassword)
- }
-
- if b != nil {
- if b.explicit.arch {
- options.Arch = b.options.Arch
- }
- if b.explicit.sign {
- options.Sign = b.options.Sign
- }
- if b.explicit.notarise {
- options.Notarise = b.options.Notarise
- }
- if b.explicit.dmg {
- options.DMG = b.options.DMG
- }
- if b.explicit.testFlight {
- options.TestFlight = b.options.TestFlight
- }
- if b.explicit.appStore {
- options.AppStore = b.options.AppStore
- }
- }
-
- if runtime != nil {
- override := *runtime
- if override.TeamID != "" {
- options.TeamID = override.TeamID
- }
- if override.BundleID != "" {
- options.BundleID = override.BundleID
- }
- if override.Arch != "" {
- options.Arch = override.Arch
- }
- if override.CertIdentity != "" {
- options.CertIdentity = override.CertIdentity
- }
- if override.ProfilePath != "" {
- options.ProfilePath = override.ProfilePath
- }
- if override.KeychainPath != "" {
- options.KeychainPath = override.KeychainPath
- }
- if override.MetadataPath != "" {
- options.MetadataPath = override.MetadataPath
- }
- if override.APIKeyID != "" {
- options.APIKeyID = override.APIKeyID
- }
- if override.APIKeyIssuerID != "" {
- options.APIKeyIssuerID = override.APIKeyIssuerID
- }
- if override.APIKeyPath != "" {
- options.APIKeyPath = override.APIKeyPath
- }
- if override.AppleID != "" {
- options.AppleID = override.AppleID
- }
- if override.Password != "" {
- options.Password = override.Password
- }
- if override.BundleDisplayName != "" {
- options.BundleDisplayName = override.BundleDisplayName
- }
- if override.MinSystemVersion != "" {
- options.MinSystemVersion = override.MinSystemVersion
- }
- if override.Category != "" {
- options.Category = override.Category
- }
- if override.Copyright != "" {
- options.Copyright = override.Copyright
- }
- if override.PrivacyPolicyURL != "" {
- options.PrivacyPolicyURL = override.PrivacyPolicyURL
- }
- if override.DMGBackground != "" {
- options.DMGBackground = override.DMGBackground
- }
- if override.DMGVolumeName != "" {
- options.DMGVolumeName = override.DMGVolumeName
- }
- if override.EntitlementsPath != "" {
- options.EntitlementsPath = override.EntitlementsPath
- }
- applyRuntimePipelineOverrides(&options, override)
- }
-
- return options
-}
-
-func applyRuntimePipelineOverrides(options *AppleOptions, override AppleOptions) {
- if options == nil {
- return
- }
-
- // Partial runtime overrides often only provide identity/metadata fields.
- // Treat all-zero booleans in that case as "unspecified" so the builder keeps
- // config/default pipeline behavior instead of disabling sign/notarise by
- // accident. Bool-only runtime structs still override everything explicitly.
- hasNonBooleanOverrides := hasNonBooleanRuntimeOverrides(override)
-
- if override.Sign || !hasNonBooleanOverrides {
- options.Sign = override.Sign
- }
- if override.Notarise || !hasNonBooleanOverrides {
- options.Notarise = override.Notarise
- }
- if override.DMG || !hasNonBooleanOverrides {
- options.DMG = override.DMG
- }
- if override.TestFlight || !hasNonBooleanOverrides {
- options.TestFlight = override.TestFlight
- }
- if override.AppStore || !hasNonBooleanOverrides {
- options.AppStore = override.AppStore
- }
-}
-
-func hasNonBooleanRuntimeOverrides(options AppleOptions) bool {
- for _, value := range []string{
- options.TeamID,
- options.BundleID,
- options.Arch,
- options.CertIdentity,
- options.ProfilePath,
- options.KeychainPath,
- options.MetadataPath,
- options.APIKeyID,
- options.APIKeyIssuerID,
- options.APIKeyPath,
- options.AppleID,
- options.Password,
- options.BundleDisplayName,
- options.MinSystemVersion,
- options.Category,
- options.Copyright,
- options.PrivacyPolicyURL,
- options.DMGBackground,
- options.DMGVolumeName,
- options.EntitlementsPath,
- } {
- if core.Trim(value) != "" {
- return true
- }
- }
-
- return false
-}
-
-func resolveBundleName(cfg *build.BuildConfig, projectDir string) string {
- if cfg != nil {
- if cfg.Project.Binary != "" {
- return cfg.Project.Binary
- }
- if cfg.Project.Name != "" {
- return cfg.Project.Name
- }
- }
- return ax.Base(projectDir)
-}
-
-func runtimeConfig(filesystem coreio.Medium, projectDir, outputDir, name string, buildConfig *build.BuildConfig, version string) *build.Config {
- return build.RuntimeConfigFromBuildConfig(filesystem, projectDir, outputDir, name, buildConfig, false, "", version)
-}
-
-var buildNumberPattern = regexp.MustCompile(`^[0-9]+$`)
-
-func resolveBuildNumber(ctx context.Context, projectDir string) core.Result {
- if value := core.Trim(core.Env("GITHUB_RUN_NUMBER")); value != "" {
- if validated := validateBuildNumber(value); validated.OK {
- return core.Ok(value)
- }
- }
-
- outputResult := runDirFn(ctx, projectDir, "git", "rev-list", "--count", "HEAD")
- if !outputResult.OK {
- return core.Ok("1")
- }
-
- value := core.Trim(outputResult.Value.(string))
- if value == "" {
- return core.Ok("1")
- }
- validated := validateBuildNumber(value)
- if !validated.OK {
- return validated
- }
- return core.Ok(value)
-}
-
-func validateBuildNumber(value string) core.Result {
- if !buildNumberPattern.MatchString(value) {
- return core.Fail(core.E("apple.validateBuildNumber", "build number must be a positive integer", nil))
- }
- return core.Ok(nil)
-}
-
-func firstNonEmpty(values ...string) string {
- for _, value := range values {
- if core.Trim(value) != "" {
- return value
- }
- }
- return ""
-}
diff --git a/pkg/build/apple/apple_example_test.go b/pkg/build/apple/apple_example_test.go
deleted file mode 100644
index 855a976..0000000
--- a/pkg/build/apple/apple_example_test.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package apple
-
-import core "dappco.re/go"
-
-// ExampleRegister references Register on this package API surface.
-func ExampleRegister() {
- _ = Register
- core.Println("Register")
- // Output: Register
-}
-
-// ExampleNew references New on this package API surface.
-func ExampleNew() {
- _ = New
- core.Println("New")
- // Output: New
-}
-
-// ExampleWithArch references WithArch on this package API surface.
-func ExampleWithArch() {
- _ = WithArch
- core.Println("WithArch")
- // Output: WithArch
-}
-
-// ExampleWithSign references WithSign on this package API surface.
-func ExampleWithSign() {
- _ = WithSign
- core.Println("WithSign")
- // Output: WithSign
-}
-
-// ExampleWithNotarise references WithNotarise on this package API surface.
-func ExampleWithNotarise() {
- _ = WithNotarise
- core.Println("WithNotarise")
- // Output: WithNotarise
-}
-
-// ExampleWithDMG references WithDMG on this package API surface.
-func ExampleWithDMG() {
- _ = WithDMG
- core.Println("WithDMG")
- // Output: WithDMG
-}
-
-// ExampleWithTestFlight references WithTestFlight on this package API surface.
-func ExampleWithTestFlight() {
- _ = WithTestFlight
- core.Println("WithTestFlight")
- // Output: WithTestFlight
-}
-
-// ExampleWithAppStore references WithAppStore on this package API surface.
-func ExampleWithAppStore() {
- _ = WithAppStore
- core.Println("WithAppStore")
- // Output: WithAppStore
-}
-
-// ExampleAppleBuilder_Name references AppleBuilder.Name on this package API surface.
-func ExampleAppleBuilder_Name() {
- _ = (*AppleBuilder).Name
- core.Println("AppleBuilder.Name")
- // Output: AppleBuilder.Name
-}
-
-// ExampleAppleBuilder_Detect references AppleBuilder.Detect on this package API surface.
-func ExampleAppleBuilder_Detect() {
- _ = (*AppleBuilder).Detect
- core.Println("AppleBuilder.Detect")
- // Output: AppleBuilder.Detect
-}
-
-// ExampleAppleBuilder_Build references AppleBuilder.Build on this package API surface.
-func ExampleAppleBuilder_Build() {
- _ = (*AppleBuilder).Build
- core.Println("AppleBuilder.Build")
- // Output: AppleBuilder.Build
-}
-
-// ExampleBuildWailsApp references BuildWailsApp on this package API surface.
-func ExampleBuildWailsApp() {
- _ = BuildWailsApp
- core.Println("BuildWailsApp")
- // Output: BuildWailsApp
-}
-
-// ExampleCreateUniversal references CreateUniversal on this package API surface.
-func ExampleCreateUniversal() {
- _ = CreateUniversal
- core.Println("CreateUniversal")
- // Output: CreateUniversal
-}
-
-// ExampleSign references Sign on this package API surface.
-func ExampleSign() {
- _ = Sign
- core.Println("Sign")
- // Output: Sign
-}
-
-// ExampleNotarise references Notarise on this package API surface.
-func ExampleNotarise() {
- _ = Notarise
- core.Println("Notarise")
- // Output: Notarise
-}
-
-// ExampleCreateDMG references CreateDMG on this package API surface.
-func ExampleCreateDMG() {
- _ = CreateDMG
- core.Println("CreateDMG")
- // Output: CreateDMG
-}
-
-// ExampleUploadTestFlight references UploadTestFlight on this package API surface.
-func ExampleUploadTestFlight() {
- _ = UploadTestFlight
- core.Println("UploadTestFlight")
- // Output: UploadTestFlight
-}
-
-// ExampleSubmitAppStore references SubmitAppStore on this package API surface.
-func ExampleSubmitAppStore() {
- _ = SubmitAppStore
- core.Println("SubmitAppStore")
- // Output: SubmitAppStore
-}
diff --git a/pkg/build/apple/apple_test.go b/pkg/build/apple/apple_test.go
deleted file mode 100644
index 2417d25..0000000
--- a/pkg/build/apple/apple_test.go
+++ /dev/null
@@ -1,1142 +0,0 @@
-package apple
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/testassert"
- build "dappco.re/go/build/pkg/build"
- "dappco.re/go/build/pkg/build/signing"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-func requireAppleOKResult(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func TestAppleBuilder_New_Good(t *testing.T) {
- builder := New(
- WithArch("arm64"),
- WithSign(false),
- WithNotarise(false),
- WithDMG(true),
- WithTestFlight(true),
- WithAppStore(true),
- )
- if stdlibAssertNil(builder) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("apple", builder.Name()) {
- t.Fatalf("want %v, got %v", "apple", builder.Name())
- }
- if stdlibAssertNil(builder.ServiceRuntime) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("arm64", builder.options.Arch) {
- t.Fatalf("want %v, got %v", "arm64", builder.options.Arch)
- }
- if !stdlibAssertEqual("arm64", builder.Options().Arch) {
- t.Fatalf("want %v, got %v", "arm64", builder.Options().Arch)
- }
- if builder.options.Sign {
- t.Fatal("expected false")
- }
- if builder.options.Notarise {
- t.Fatal("expected false")
- }
- if !(builder.options.DMG) {
- t.Fatal("expected true")
- }
- if !(builder.options.TestFlight) {
- t.Fatal("expected true")
- }
- if !(builder.options.AppStore) {
- t.Fatal("expected true")
- }
- if !(builder.explicit.arch) {
- t.Fatal("expected true")
- }
- if !(builder.explicit.sign) {
- t.Fatal("expected true")
- }
- if !(builder.explicit.notarise) {
- t.Fatal("expected true")
- }
- if !(builder.explicit.dmg) {
- t.Fatal("expected true")
- }
- if !(builder.explicit.testFlight) {
- t.Fatal("expected true")
- }
- if !(builder.explicit.appStore) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestAppleBuilder_New_PreservesExplicitDefaultValuedOptions_Good(t *testing.T) {
- builder := New(
- WithArch("universal"),
- WithSign(true),
- WithNotarise(true),
- )
- if stdlibAssertNil(builder) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("universal", builder.options.Arch) {
- t.Fatalf("want %v, got %v", "universal", builder.options.Arch)
- }
- if !(builder.options.Sign) {
- t.Fatal("expected true")
- }
- if !(builder.options.Notarise) {
- t.Fatal("expected true")
- }
- if !(builder.explicit.arch) {
- t.Fatal("expected true")
- }
- if !(builder.explicit.sign) {
- t.Fatal("expected true")
- }
- if !(builder.explicit.notarise) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestAppleBuilder_Register_Good(t *testing.T) {
- c := core.New()
-
- result := Register(c)
- if !(result.OK) {
- t.Fatal("expected true")
- }
-
- builder, ok := result.Value.(*AppleBuilder)
- if !(ok) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("apple", builder.Name()) {
- t.Fatalf("want %v, got %v", "apple", builder.Name())
- }
- if stdlibAssertNil(builder.ServiceRuntime) {
- t.Fatal("expected non-nil")
- }
- if c != builder.Core() {
- t.Fatalf("expected %v and %v to be the same", c, builder.Core())
- }
- if !(c.Service("apple").OK) {
- t.Fatal("expected true")
- }
- if !(c.RegistryOf("services").Has("apple")) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestAppleBuilder_Detect_Good(t *testing.T) {
- dir := t.TempDir()
- requireAppleOKResult(t, ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644))
-
- result := New().Detect(coreio.Local, dir)
- if !(result.OK) {
- t.Fatal("expected true")
- }
-
- detected, ok := result.Value.(bool)
- if !(ok) {
- t.Fatal("expected true")
- }
- if !(detected) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestAppleBuilder_Build_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- oldLoadConfig := loadConfigFn
- oldBuildApple := buildAppleFn
- oldDetermineVersion := determineVersion
- oldGetwd := getwdFn
- oldRunDir := runDirFn
- oldWriteXcodeCloudScripts := writeXcodeCloudScriptsFn
- t.Cleanup(func() {
- loadConfigFn = oldLoadConfig
- buildAppleFn = oldBuildApple
- determineVersion = oldDetermineVersion
- getwdFn = oldGetwd
- runDirFn = oldRunDir
- writeXcodeCloudScriptsFn = oldWriteXcodeCloudScripts
- })
-
- loadConfigFn = func(fs coreio.Medium, dir string) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
-
- return core.Ok(&build.BuildConfig{
- Project: build.Project{
- Name: "Core",
- Binary: "Core",
- },
- Build: build.Build{
- LDFlags: []string{"-s", "-w"},
- },
- Apple: build.AppleConfig{
- BundleID: "ai.lthn.core",
- Sign: boolPtr(false),
- },
- Sign: signing.SignConfig{
- MacOS: signing.MacOSConfig{
- Identity: "Developer ID Application: Lethean CIC (ABC123DEF4)",
- TeamID: "ABC123DEF4",
- },
- },
- })
- }
- determineVersion = func(ctx context.Context, dir string) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
-
- return core.Ok("v1.2.3")
- }
- getwdFn = func() core.Result {
- return core.Ok(projectDir)
- }
- runDirFn = func(ctx context.Context, dir, command string, args ...string) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
- if !stdlibAssertEqual("git", command) {
- t.Fatalf("want %v, got %v", "git", command)
- }
- if !stdlibAssertEqual([]string{"rev-list", "--count", "HEAD"}, args) {
- t.Fatalf("want %v, got %v", []string{"rev-list", "--count", "HEAD"}, args)
- }
-
- return core.Ok("42")
- }
- buildAppleFn = func(ctx context.Context, cfg *build.Config, options build.AppleOptions, buildNumber string) core.Result {
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", "apple"), cfg.OutputDir) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", "apple"), cfg.OutputDir)
- }
- if !stdlibAssertEqual("Core", cfg.Name) {
- t.Fatalf("want %v, got %v", "Core", cfg.Name)
- }
- if !stdlibAssertEqual("v1.2.3", cfg.Version) {
- t.Fatalf("want %v, got %v", "v1.2.3", cfg.Version)
- }
- if !stdlibAssertEqual("42", buildNumber) {
- t.Fatalf("want %v, got %v", "42", buildNumber)
- }
- if !stdlibAssertEqual("ai.lthn.core", options.BundleID) {
- t.Fatalf("want %v, got %v", "ai.lthn.core", options.BundleID)
- }
- if !(options.Sign) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("arm64", options.Arch) {
- t.Fatalf("want %v, got %v", "arm64", options.Arch)
- }
-
- return core.Ok(&build.AppleBuildResult{
- BundlePath: ax.Join(cfg.OutputDir, "Core.app"),
- })
- }
-
- result := New(WithArch("arm64"), WithSign(true)).Build(context.Background(), nil)
- if !(result.OK) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", "apple", "Core.app"), result.Value) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", "apple", "Core.app"), result.Value)
- }
-
-}
-
-func TestAppleBuilder_Build_PartialRuntimeOptionsPreservePipelineDefaults_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- oldLoadConfig := loadConfigFn
- oldBuildApple := buildAppleFn
- oldDetermineVersion := determineVersion
- oldGetwd := getwdFn
- oldRunDir := runDirFn
- t.Cleanup(func() {
- loadConfigFn = oldLoadConfig
- buildAppleFn = oldBuildApple
- determineVersion = oldDetermineVersion
- getwdFn = oldGetwd
- runDirFn = oldRunDir
- })
-
- loadConfigFn = func(fs coreio.Medium, dir string) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
-
- return core.Ok(&build.BuildConfig{
- Project: build.Project{
- Name: "Core",
- Binary: "Core",
- },
- Apple: build.AppleConfig{
- BundleID: "ai.lthn.core",
- DMG: boolPtr(true),
- },
- Sign: signing.SignConfig{
- MacOS: signing.MacOSConfig{
- Identity: "Developer ID Application: Lethean CIC (ABC123DEF4)",
- TeamID: "ABC123DEF4",
- },
- },
- })
- }
- determineVersion = func(ctx context.Context, dir string) core.Result {
- return core.Ok("v1.2.3")
- }
- getwdFn = func() core.Result {
- return core.Ok(projectDir)
- }
- runDirFn = func(ctx context.Context, dir, command string, args ...string) core.Result {
- return core.Ok("42")
- }
- buildAppleFn = func(ctx context.Context, cfg *build.Config, options build.AppleOptions, buildNumber string) core.Result {
- if !stdlibAssertEqual("ai.lthn.override", options.BundleID) {
- t.Fatalf("want %v, got %v", "ai.lthn.override", options.BundleID)
- }
- if !(options.Sign) {
- t.Fatal("expected true")
- }
- if !(options.Notarise) {
- t.Fatal("expected true")
- }
- if !(options.DMG) {
- t.Fatal("expected true")
- }
- if options.TestFlight {
- t.Fatal("expected false")
- }
- if options.AppStore {
- t.Fatal("expected false")
- }
-
- return core.Ok(&build.AppleBuildResult{
- BundlePath: ax.Join(cfg.OutputDir, "Core.app"),
- })
- }
-
- result := New().Build(context.Background(), &AppleOptions{
- BundleID: "ai.lthn.override",
- })
- if !(result.OK) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", "apple", "Core.app"), result.Value) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", "apple", "Core.app"), result.Value)
- }
-
-}
-
-func TestAppleBuilder_Build_SetsUpBuildCache_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- oldLoadConfig := loadConfigFn
- oldBuildApple := buildAppleFn
- oldDetermineVersion := determineVersion
- oldGetwd := getwdFn
- oldRunDir := runDirFn
- t.Cleanup(func() {
- loadConfigFn = oldLoadConfig
- buildAppleFn = oldBuildApple
- determineVersion = oldDetermineVersion
- getwdFn = oldGetwd
- runDirFn = oldRunDir
- })
-
- loadConfigFn = func(fs coreio.Medium, dir string) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
-
- return core.Ok(&build.BuildConfig{
- Project: build.Project{
- Name: "Core",
- Binary: "Core",
- },
- Build: build.Build{
- Cache: build.CacheConfig{
- Enabled: true,
- Paths: []string{
- "cache/go-build",
- "cache/go-mod",
- },
- },
- },
- Apple: build.AppleConfig{
- BundleID: "ai.lthn.core",
- Sign: boolPtr(false),
- },
- })
- }
- determineVersion = func(ctx context.Context, dir string) core.Result {
- return core.Ok("v1.2.3")
- }
- getwdFn = func() core.Result {
- return core.Ok(projectDir)
- }
- runDirFn = func(ctx context.Context, dir, command string, args ...string) core.Result {
- return core.Ok("42")
- }
- buildAppleFn = func(ctx context.Context, cfg *build.Config, options build.AppleOptions, buildNumber string) core.Result {
- if !stdlibAssertEqual([]string{ax.Join(projectDir, "cache", "go-build"), ax.Join(projectDir, "cache", "go-mod")}, cfg.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{ax.Join(projectDir, "cache", "go-build"), ax.Join(projectDir, "cache", "go-mod")}, cfg.Cache.Paths)
- }
- if !(cfg.FS.Exists(ax.Join(projectDir, ".core", "cache"))) {
- t.Fatal("expected true")
- }
- if !(cfg.FS.Exists(ax.Join(projectDir, "cache", "go-build"))) {
- t.Fatal("expected true")
- }
- if !(cfg.FS.Exists(ax.Join(projectDir, "cache", "go-mod"))) {
- t.Fatal("expected true")
- }
-
- return core.Ok(&build.AppleBuildResult{
- BundlePath: ax.Join(cfg.OutputDir, "Core.app"),
- })
- }
-
- result := New().Build(context.Background(), nil)
- if !(result.OK) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", "apple", "Core.app"), result.Value) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", "apple", "Core.app"), result.Value)
- }
-
-}
-
-func TestAppleBuilder_Build_WritesXcodeCloudScripts_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- oldLoadConfig := loadConfigFn
- oldBuildApple := buildAppleFn
- oldDetermineVersion := determineVersion
- oldGetwd := getwdFn
- oldRunDir := runDirFn
- oldWriteXcodeCloudScripts := writeXcodeCloudScriptsFn
- t.Cleanup(func() {
- loadConfigFn = oldLoadConfig
- buildAppleFn = oldBuildApple
- determineVersion = oldDetermineVersion
- getwdFn = oldGetwd
- runDirFn = oldRunDir
- writeXcodeCloudScriptsFn = oldWriteXcodeCloudScripts
- })
-
- loadConfigFn = func(fs coreio.Medium, dir string) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
-
- return core.Ok(&build.BuildConfig{
- Project: build.Project{
- Name: "Core",
- Binary: "Core",
- },
- Apple: build.AppleConfig{
- BundleID: "ai.lthn.core",
- Sign: boolPtr(false),
- XcodeCloud: build.XcodeCloudConfig{
- Workflow: "CoreGUI Release",
- },
- },
- })
- }
- determineVersion = func(ctx context.Context, dir string) core.Result {
- return core.Ok("v1.2.3")
- }
- getwdFn = func() core.Result {
- return core.Ok(projectDir)
- }
- runDirFn = func(ctx context.Context, dir, command string, args ...string) core.Result {
- return core.Ok("42")
- }
-
- var scriptsWritten bool
- writeXcodeCloudScriptsFn = func(fs coreio.Medium, dir string, cfg *build.BuildConfig) core.Result {
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
- if !stdlibAssertEqual("CoreGUI Release", cfg.Apple.XcodeCloud.Workflow) {
- t.Fatalf("want %v, got %v", "CoreGUI Release", cfg.Apple.XcodeCloud.Workflow)
- }
-
- scriptsWritten = true
- return core.Ok([]string{ax.Join(dir, build.XcodeCloudScriptsDir, build.XcodeCloudPreXcodebuildScriptName)})
- }
- buildAppleFn = func(ctx context.Context, cfg *build.Config, options build.AppleOptions, buildNumber string) core.Result {
- return core.Ok(&build.AppleBuildResult{
- BundlePath: ax.Join(cfg.OutputDir, "Core.app"),
- })
- }
-
- result := New().Build(context.Background(), nil)
- if !(result.OK) {
- t.Fatal("expected true")
- }
- if !(scriptsWritten) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", "apple", "Core.app"), result.Value) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", "apple", "Core.app"), result.Value)
- }
-
-}
-
-func TestAppleBuilder_resolveOptions_BoolOnlyRuntimeOverride_Good(t *testing.T) {
- builder := New()
-
- options := builder.resolveOptions(&build.BuildConfig{
- Apple: build.AppleConfig{
- BundleID: "ai.lthn.core",
- DMG: boolPtr(true),
- },
- }, &AppleOptions{
- Sign: false,
- Notarise: false,
- DMG: false,
- AppStore: true,
- })
- if options.Sign {
- t.Fatal("expected false")
- }
- if options.Notarise {
- t.Fatal("expected false")
- }
- if options.DMG {
- t.Fatal("expected false")
- }
- if !(options.AppStore) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestApple_BuildWailsApp_UsesCurrentDirectoryAndStringLDFlags_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- oldBuildWails := buildWailsAppFn
- oldGetwd := getwdFn
- t.Cleanup(func() {
- buildWailsAppFn = oldBuildWails
- getwdFn = oldGetwd
- })
-
- getwdFn = func() core.Result {
- return core.Ok(projectDir)
- }
-
- buildWailsAppFn = func(ctx context.Context, cfg build.WailsBuildConfig) core.Result {
- if !stdlibAssertEqual(projectDir, cfg.ProjectDir) {
- t.Fatalf("want %v, got %v", projectDir, cfg.ProjectDir)
- }
- if !stdlibAssertEqual("Core", cfg.Name) {
- t.Fatalf("want %v, got %v", "Core", cfg.Name)
- }
- if !stdlibAssertEqual("arm64", cfg.Arch) {
- t.Fatalf("want %v, got %v", "arm64", cfg.Arch)
- }
- if !stdlibAssertEqual([]string{"integration"}, cfg.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"integration"}, cfg.BuildTags)
- }
- if !stdlibAssertEqual([]string{"-s -w -X main.version=1.2.3"}, cfg.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s -w -X main.version=1.2.3"}, cfg.LDFlags)
- }
- if !stdlibAssertEqual("1.2.3", cfg.Version) {
- t.Fatalf("want %v, got %v", "1.2.3", cfg.Version)
- }
- if !stdlibAssertEqual([]string{"FOO=bar"}, cfg.Env) {
- t.Fatalf("want %v, got %v", []string{"FOO=bar"}, cfg.Env)
- }
-
- return core.Ok(ax.Join(projectDir, "dist", "Core.app"))
- }
-
- result := BuildWailsApp(context.Background(), WailsBuildConfig{
- Name: "Core",
- Arch: "arm64",
- BuildTags: []string{"integration"},
- LDFlags: "-s -w -X main.version=1.2.3",
- OutputDir: ax.Join(projectDir, "dist"),
- Version: "1.2.3",
- Env: []string{"FOO=bar"},
- })
- if !(result.OK) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", "Core.app"), result.Value) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", "Core.app"), result.Value)
- }
-
-}
-
-func boolPtr(value bool) *bool {
- return &value
-}
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
-
-// --- v0.9.0 generated compliance triplets ---
-func TestApple_Register_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Register(core.New())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_Register_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Register(core.New())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_Register_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Register(core.New())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_New_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = New()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_New_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = New()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_New_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = New()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithArch_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithArch("amd64")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithArch_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithArch("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithArch_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithArch("amd64")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithSign_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithSign(true)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithSign_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithSign(false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithSign_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithSign(true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithNotarise_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithNotarise(true)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithNotarise_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithNotarise(false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithNotarise_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithNotarise(true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithDMG_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithDMG(true)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithDMG_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithDMG(false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithDMG_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithDMG(true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithTestFlight_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithTestFlight(true)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithTestFlight_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithTestFlight(false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithTestFlight_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithTestFlight(true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithAppStore_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppStore(true)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithAppStore_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppStore(false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithAppStore_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppStore(true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_AppleBuilder_Name_Good(t *core.T) {
- subject := &AppleBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleBuilder_Name_Bad(t *core.T) {
- subject := &AppleBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleBuilder_Name_Ugly(t *core.T) {
- subject := &AppleBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_AppleBuilder_Detect_Good(t *core.T) {
- subject := &AppleBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(coreio.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleBuilder_Detect_Bad(t *core.T) {
- subject := &AppleBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(coreio.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleBuilder_Detect_Ugly(t *core.T) {
- subject := &AppleBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(coreio.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_AppleBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &AppleBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, &AppleOptions{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &AppleBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &AppleBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, &AppleOptions{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_BuildWailsApp_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = BuildWailsApp(ctx, WailsBuildConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_BuildWailsApp_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = BuildWailsApp(ctx, WailsBuildConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_BuildWailsApp_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = BuildWailsApp(ctx, WailsBuildConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_CreateUniversal_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateUniversal(core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_CreateUniversal_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateUniversal("", "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_CreateUniversal_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateUniversal(core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_Sign_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Sign(ctx, SignConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_Sign_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Sign(ctx, SignConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_Sign_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Sign(ctx, SignConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_Notarise_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Notarise(ctx, NotariseConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_Notarise_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Notarise(ctx, NotariseConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_Notarise_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Notarise(ctx, NotariseConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_CreateDMG_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateDMG(ctx, DMGConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_CreateDMG_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateDMG(ctx, DMGConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_CreateDMG_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateDMG(ctx, DMGConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_UploadTestFlight_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = UploadTestFlight(ctx, TestFlightConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_UploadTestFlight_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = UploadTestFlight(ctx, TestFlightConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_UploadTestFlight_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = UploadTestFlight(ctx, TestFlightConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_SubmitAppStore_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = SubmitAppStore(ctx, AppStoreConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_SubmitAppStore_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = SubmitAppStore(ctx, AppStoreConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_SubmitAppStore_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = SubmitAppStore(ctx, AppStoreConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/apple_example_test.go b/pkg/build/apple_example_test.go
deleted file mode 100644
index 50c132c..0000000
--- a/pkg/build/apple_example_test.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleDefaultAppleOptions references DefaultAppleOptions on this package API surface.
-func ExampleDefaultAppleOptions() {
- _ = DefaultAppleOptions
- core.Println("DefaultAppleOptions")
- // Output: DefaultAppleOptions
-}
-
-// ExampleAppleConfig_Resolve references AppleConfig.Resolve on this package API surface.
-func ExampleAppleConfig_Resolve() {
- _ = (*AppleConfig).Resolve
- core.Println("AppleConfig.Resolve")
- // Output: AppleConfig.Resolve
-}
-
-// ExampleBuildApple references BuildApple on this package API surface.
-func ExampleBuildApple() {
- _ = BuildApple
- core.Println("BuildApple")
- // Output: BuildApple
-}
-
-// ExampleBuildWailsApp references BuildWailsApp on this package API surface.
-func ExampleBuildWailsApp() {
- _ = BuildWailsApp
- core.Println("BuildWailsApp")
- // Output: BuildWailsApp
-}
-
-// ExampleCreateUniversal references CreateUniversal on this package API surface.
-func ExampleCreateUniversal() {
- _ = CreateUniversal
- core.Println("CreateUniversal")
- // Output: CreateUniversal
-}
-
-// ExampleSign references Sign on this package API surface.
-func ExampleSign() {
- _ = Sign
- core.Println("Sign")
- // Output: Sign
-}
-
-// ExampleNotarise references Notarise on this package API surface.
-func ExampleNotarise() {
- _ = Notarise
- core.Println("Notarise")
- // Output: Notarise
-}
-
-// ExampleCreateDMG references CreateDMG on this package API surface.
-func ExampleCreateDMG() {
- _ = CreateDMG
- core.Println("CreateDMG")
- // Output: CreateDMG
-}
-
-// ExampleUploadTestFlight references UploadTestFlight on this package API surface.
-func ExampleUploadTestFlight() {
- _ = UploadTestFlight
- core.Println("UploadTestFlight")
- // Output: UploadTestFlight
-}
-
-// ExampleSubmitAppStore references SubmitAppStore on this package API surface.
-func ExampleSubmitAppStore() {
- _ = SubmitAppStore
- core.Println("SubmitAppStore")
- // Output: SubmitAppStore
-}
-
-// ExampleWriteInfoPlist references WriteInfoPlist on this package API surface.
-func ExampleWriteInfoPlist() {
- _ = WriteInfoPlist
- core.Println("WriteInfoPlist")
- // Output: WriteInfoPlist
-}
-
-// ExampleWriteEntitlements references WriteEntitlements on this package API surface.
-func ExampleWriteEntitlements() {
- _ = WriteEntitlements
- core.Println("WriteEntitlements")
- // Output: WriteEntitlements
-}
-
-// ExampleInfoPlist_Values references InfoPlist.Values on this package API surface.
-func ExampleInfoPlist_Values() {
- _ = (*InfoPlist).Values
- core.Println("InfoPlist.Values")
- // Output: InfoPlist.Values
-}
-
-// ExampleEntitlements_Values references Entitlements.Values on this package API surface.
-func ExampleEntitlements_Values() {
- _ = (*Entitlements).Values
- core.Println("Entitlements.Values")
- // Output: Entitlements.Values
-}
diff --git a/pkg/build/apple_test.go b/pkg/build/apple_test.go
deleted file mode 100644
index 2bd749c..0000000
--- a/pkg/build/apple_test.go
+++ /dev/null
@@ -1,1591 +0,0 @@
-package build
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func requireAppleString(t *testing.T, result core.Result) string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func requireAppleBytes(t *testing.T, result core.Result) []byte {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]byte)
-}
-
-func requireAppleBuildResult(t *testing.T, result core.Result) *AppleBuildResult {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(*AppleBuildResult)
-}
-
-func requireAppleStrings(t *testing.T, result core.Result) []string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]string)
-}
-
-func requireAppleASCPackage(t *testing.T, result core.Result) ascUploadPackage {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(ascUploadPackage)
-}
-
-func TestApple_WriteInfoPlist_Good(t *testing.T) {
- appPath := ax.Join(t.TempDir(), "Core.app")
-
- path := requireAppleString(t, WriteInfoPlist(storage.Local, appPath, InfoPlist{
- BundleID: "ai.lthn.core",
- BundleName: "Core",
- BundleDisplayName: "Core by Lethean",
- BundleVersion: "1.2.3",
- BuildNumber: "42",
- MinSystemVersion: "13.0",
- Category: "public.app-category.developer-tools",
- Copyright: "Copyright 2026 Lethean CIC. EUPL-1.2.",
- Executable: "Core",
- HighResCapable: true,
- SupportsSecureRestorableState: true,
- }))
-
- content := requireAppleString(t, storage.Local.Read(path))
- if !stdlibAssertContains(content, "CFBundleIdentifier") {
- t.Fatalf("expected %v to contain %v", content, "CFBundleIdentifier")
- }
- if !stdlibAssertContains(content, "ai.lthn.core") {
- t.Fatalf("expected %v to contain %v", content, "ai.lthn.core")
- }
- if !stdlibAssertContains(content, "CFBundleExecutable") {
- t.Fatalf("expected %v to contain %v", content, "CFBundleExecutable")
- }
- if !stdlibAssertContains(content, "Core") {
- t.Fatalf("expected %v to contain %v", content, "Core")
- }
-
-}
-
-func TestApple_CreateUniversal_Good(t *testing.T) {
- dir := t.TempDir()
- arm64Path := ax.Join(dir, "arm64", "Core.app")
- amd64Path := ax.Join(dir, "amd64", "Core.app")
- outputPath := ax.Join(dir, "universal", "Core.app")
-
- writeDummyAppBundle(t, arm64Path, "Core", "arm64")
- writeDummyAppBundle(t, amd64Path, "Core", "amd64")
-
- oldResolve := appleResolveCommand
- oldCombined := appleCombinedOutput
- t.Cleanup(func() {
- appleResolveCommand = oldResolve
- appleCombinedOutput = oldCombined
- })
-
- appleResolveCommand = func(name string, fallbackPaths ...string) core.Result {
- return core.Ok(name)
- }
- appleCombinedOutput = func(ctx context.Context, dir string, env []string, command string, args ...string) core.Result {
- if !stdlibAssertEqual("lipo", command) {
- t.Fatalf("want %v, got %v", "lipo", command)
- }
- if !stdlibAssertEqual([]string{"-create", "-output", ax.Join(outputPath, "Contents", "MacOS", "Core"), ax.Join(arm64Path, "Contents", "MacOS", "Core"), ax.Join(amd64Path, "Contents", "MacOS", "Core")}, args) {
- t.Fatalf("want %v, got %v", []string{"-create", "-output", ax.Join(outputPath, "Contents", "MacOS", "Core"), ax.Join(arm64Path, "Contents", "MacOS", "Core"), ax.Join(amd64Path, "Contents", "MacOS", "Core")}, args)
- }
- result := ax.WriteFile(ax.Join(outputPath, "Contents", "MacOS", "Core"), []byte("universal"), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return core.Ok("")
- }
-
- result := CreateUniversal(arm64Path, amd64Path, outputPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- content := requireAppleBytes(t, ax.ReadFile(ax.Join(outputPath, "Contents", "MacOS", "Core")))
- if !stdlibAssertEqual("universal", string(content)) {
- t.Fatalf("want %v, got %v", "universal", string(content))
- }
-
-}
-
-func TestApple_CreateUniversal_MergesHelpersAndFrameworks_Good(t *testing.T) {
- dir := t.TempDir()
- arm64Path := ax.Join(dir, "arm64", "Core.app")
- amd64Path := ax.Join(dir, "amd64", "Core.app")
- outputPath := ax.Join(dir, "universal", "Core.app")
-
- writeDummyAppBundle(t, arm64Path, "Core", "arm64-main")
- writeDummyAppBundle(t, amd64Path, "Core", "amd64-main")
- writeDummyExecutable(t, ax.Join(arm64Path, "Contents", "MacOS", "Core Helper"), "arm64-helper")
- writeDummyExecutable(t, ax.Join(amd64Path, "Contents", "MacOS", "Core Helper"), "amd64-helper")
- writeDummyExecutable(t, ax.Join(arm64Path, "Contents", "Frameworks", "Example.framework", "Example"), "arm64-framework")
- writeDummyExecutable(t, ax.Join(amd64Path, "Contents", "Frameworks", "Example.framework", "Example"), "amd64-framework")
- writeDummyExecutable(t, ax.Join(arm64Path, "Contents", "Frameworks", "libSupport.dylib"), "arm64-dylib")
- writeDummyExecutable(t, ax.Join(amd64Path, "Contents", "Frameworks", "libSupport.dylib"), "amd64-dylib")
-
- oldResolve := appleResolveCommand
- oldCombined := appleCombinedOutput
- t.Cleanup(func() {
- appleResolveCommand = oldResolve
- appleCombinedOutput = oldCombined
- })
-
- var mergedOutputs []string
- appleResolveCommand = func(name string, fallbackPaths ...string) core.Result {
- return core.Ok(name)
- }
- appleCombinedOutput = func(ctx context.Context, dir string, env []string, command string, args ...string) core.Result {
- if !stdlibAssertEqual("lipo", command) {
- t.Fatalf("want %v, got %v", "lipo", command)
- }
- if len(args) != 5 {
- t.Fatalf("want len %v, got %v", 5, len(args))
- }
- if !stdlibAssertEqual("-create", args[0]) {
- t.Fatalf("want %v, got %v", "-create", args[0])
- }
- if !stdlibAssertEqual("-output", args[1]) {
- t.Fatalf("want %v, got %v", "-output", args[1])
- }
-
- mergedOutputs = append(mergedOutputs, args[2])
- result := ax.WriteFile(args[2], []byte("universal"), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return core.Ok("")
- }
-
- result := CreateUniversal(arm64Path, amd64Path, outputPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if !stdlibAssertEqual([]string{ax.Join(outputPath, "Contents", "Frameworks", "Example.framework", "Example"), ax.Join(outputPath, "Contents", "Frameworks", "libSupport.dylib"), ax.Join(outputPath, "Contents", "MacOS", "Core"), ax.Join(outputPath, "Contents", "MacOS", "Core Helper")}, mergedOutputs) {
- t.Fatalf("want %v, got %v", []string{ax.Join(outputPath, "Contents", "Frameworks", "Example.framework", "Example"), ax.Join(outputPath, "Contents", "Frameworks", "libSupport.dylib"), ax.Join(outputPath, "Contents", "MacOS", "Core"), ax.Join(outputPath, "Contents", "MacOS", "Core Helper")}, mergedOutputs)
- }
-
- for _, path := range mergedOutputs {
- content := requireAppleBytes(t, ax.ReadFile(path))
- if !stdlibAssertEqual("universal", string(content)) {
- t.Fatalf("want %v, got %v", "universal", string(content))
- }
-
- }
-}
-
-func TestApple_NormaliseDMGConfig_DefaultsGood(t *testing.T) {
- cfg := normaliseDMGConfig(DMGConfig{
- AppPath: ax.Join("/tmp", "Core.app"),
- })
- if !stdlibAssertEqual("Core", cfg.VolumeName) {
- t.Fatalf("want %v, got %v", "Core", cfg.VolumeName)
- }
- if !stdlibAssertEqual(defaultDMGIconSize, cfg.IconSize) {
- t.Fatalf("want %v, got %v", defaultDMGIconSize, cfg.IconSize)
- }
- if !stdlibAssertEqual([2]int{defaultDMGWindowWidth, defaultDMGWindowHeight}, cfg.WindowSize) {
- t.Fatalf("want %v, got %v", [2]int{defaultDMGWindowWidth, defaultDMGWindowHeight}, cfg.WindowSize)
- }
-
-}
-
-func TestApple_BuildDMGAppleScript_UsesConfiguredLayoutGood(t *testing.T) {
- script := buildDMGAppleScript("Core", "Core.app", DMGConfig{
- AppPath: ax.Join("/tmp", "Core.app"),
- Background: "assets/dmg-background.png",
- IconSize: 144,
- WindowSize: [2]int{800, 520},
- })
- if !stdlibAssertContains(script, `tell disk "Core"`) {
- t.Fatalf("expected %v to contain %v", script, `tell disk "Core"`)
- }
- if !stdlibAssertContains(script, "set bounds of container window to {100, 100, 900, 620}") {
- t.Fatalf("expected %v to contain %v", script, "set bounds of container window to {100, 100, 900, 620}")
- }
- if !stdlibAssertContains(script, "set icon size of opts to 144") {
- t.Fatalf("expected %v to contain %v", script, "set icon size of opts to 144")
- }
- if !stdlibAssertContains(script, `set background picture of opts to file ".background:dmg-background.png"`) {
- t.Fatalf("expected %v to contain %v", script, `set background picture of opts to file ".background:dmg-background.png"`)
- }
- if !stdlibAssertContains(script, `set position of item "Core.app" of container window to {200, 260}`) {
- t.Fatalf("expected %v to contain %v", script, `set position of item "Core.app" of container window to {200, 260}`)
- }
- if !stdlibAssertContains(script, `set position of item "Applications" of container window to {600, 260}`) {
- t.Fatalf("expected %v to contain %v", script, `set position of item "Applications" of container window to {600, 260}`)
- }
-
-}
-
-func TestApple_CreateDMG_ConfiguresFinderLayout_Good(t *testing.T) {
- projectDir := t.TempDir()
- appPath := ax.Join(projectDir, "Core.app")
- backgroundPath := ax.Join(projectDir, "assets", "dmg-background.png")
- outputPath := ax.Join(projectDir, "dist", "Core.dmg")
-
- writeDummyAppBundle(t, appPath, "Core", "built")
- result := storage.Local.EnsureDir(ax.Dir(backgroundPath))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(backgroundPath, []byte("background"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- oldResolve := appleResolveCommand
- oldCombined := appleCombinedOutput
- t.Cleanup(func() {
- appleResolveCommand = oldResolve
- appleCombinedOutput = oldCombined
- })
-
- var commands []struct {
- command string
- args []string
- }
-
- appleResolveCommand = func(name string, fallbackPaths ...string) core.Result {
- return core.Ok(name)
- }
- appleCombinedOutput = func(ctx context.Context, dir string, env []string, command string, args ...string) core.Result {
- commands = append(commands, struct {
- command string
- args []string
- }{
- command: command,
- args: append([]string{}, args...),
- })
-
- switch command {
- case "hdiutil":
- if stdlibAssertEmpty(args) {
- t.Fatal("expected non-empty")
- }
-
- switch args[0] {
- case "create":
- srcIndex := indexOf(args, "-srcfolder")
- if srcIndex < 0 {
- t.Fatalf("expected %v to be greater than or equal to %v", srcIndex, 0)
- }
-
- stageDir := args[srcIndex+1]
- if !(storage.Local.Exists(ax.Join(stageDir, "Core.app"))) {
- t.Fatal("expected true")
- }
-
- linkTarget := requireAppleString(t, ax.Readlink(ax.Join(stageDir, "Applications")))
- if !stdlibAssertEqual("/Applications", linkTarget) {
- t.Fatalf("want %v, got %v", "/Applications", linkTarget)
- }
-
- backgroundContent := requireAppleString(t, storage.Local.Read(ax.Join(stageDir, ".background", "dmg-background.png")))
- if !stdlibAssertEqual("background", backgroundContent) {
- t.Fatalf("want %v, got %v", "background", backgroundContent)
- }
-
- case "attach":
- if !stdlibAssertContains(args, "-mountpoint") {
- t.Fatalf("expected %v to contain %v", args, "-mountpoint")
- }
-
- case "detach":
- if !stdlibAssertEqual("detach", args[0]) {
- t.Fatalf("want %v, got %v", "detach", args[0])
- }
-
- case "convert":
- if !stdlibAssertEqual(outputPath, args[len(args)-1]) {
- t.Fatalf("want %v, got %v", outputPath, args[len(args)-1])
- }
-
- default:
- t.Fatalf("unexpected hdiutil command: %v", args)
- }
- case "osascript":
- if len(args) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(args))
- }
-
- script := requireAppleString(t, storage.Local.Read(args[0]))
- if !stdlibAssertContains(script, "set bounds of container window to {100, 100, 740, 580}") {
- t.Fatalf("expected %v to contain %v", script, "set bounds of container window to {100, 100, 740, 580}")
- }
- if !stdlibAssertContains(script, "set icon size of opts to 144") {
- t.Fatalf("expected %v to contain %v", script, "set icon size of opts to 144")
- }
- if !stdlibAssertContains(script, `set background picture of opts to file ".background:dmg-background.png"`) {
- t.Fatalf("expected %v to contain %v", script, `set background picture of opts to file ".background:dmg-background.png"`)
- }
- if !stdlibAssertContains(script, `set position of item "Core.app" of container window to {176, 240}`) {
- t.Fatalf("expected %v to contain %v", script, `set position of item "Core.app" of container window to {176, 240}`)
- }
- if !stdlibAssertContains(script, `set position of item "Applications" of container window to {480, 240}`) {
- t.Fatalf("expected %v to contain %v", script, `set position of item "Applications" of container window to {480, 240}`)
- }
-
- default:
- t.Fatalf("unexpected command: %s", command)
- }
-
- return core.Ok("")
- }
-
- result = CreateDMG(context.Background(), DMGConfig{
- AppPath: appPath,
- OutputPath: outputPath,
- VolumeName: "Core",
- Background: backgroundPath,
- IconSize: 144,
- WindowSize: [2]int{640, 480},
- })
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if len(commands) != 5 {
- t.Fatalf("want len %v, got %v", 5, len(commands))
- }
- if !stdlibAssertEqual("hdiutil", commands[0].command) {
- t.Fatalf("want %v, got %v", "hdiutil", commands[0].command)
- }
- if !stdlibAssertEqual("create", commands[0].args[0]) {
- t.Fatalf("want %v, got %v", "create", commands[0].args[0])
- }
- if !stdlibAssertEqual("hdiutil", commands[1].command) {
- t.Fatalf("want %v, got %v", "hdiutil", commands[1].command)
- }
- if !stdlibAssertEqual("attach", commands[1].args[0]) {
- t.Fatalf("want %v, got %v", "attach", commands[1].args[0])
- }
- if !stdlibAssertEqual("osascript", commands[2].command) {
- t.Fatalf("want %v, got %v", "osascript", commands[2].command)
- }
- if !stdlibAssertEqual("hdiutil", commands[3].command) {
- t.Fatalf("want %v, got %v", "hdiutil", commands[3].command)
- }
- if !stdlibAssertEqual("detach", commands[3].args[0]) {
- t.Fatalf("want %v, got %v", "detach", commands[3].args[0])
- }
- if !stdlibAssertEqual("hdiutil", commands[4].command) {
- t.Fatalf("want %v, got %v", "hdiutil", commands[4].command)
- }
- if !stdlibAssertEqual("convert", commands[4].args[0]) {
- t.Fatalf("want %v, got %v", "convert", commands[4].args[0])
- }
-
-}
-
-func TestApple_BuildWailsApp_AddsMLXBuildTag_Good(t *testing.T) {
- projectDir := t.TempDir()
- bundlePath := ax.Join(projectDir, "build", "bin", "Core.app")
- writeDummyAppBundle(t, bundlePath, "Core", "built")
-
- oldResolve := appleResolveCommand
- oldCombined := appleCombinedOutput
- t.Cleanup(func() {
- appleResolveCommand = oldResolve
- appleCombinedOutput = oldCombined
- })
-
- appleResolveCommand = func(name string, fallbackPaths ...string) core.Result {
- return core.Ok(name)
- }
- appleCombinedOutput = func(ctx context.Context, dir string, env []string, command string, args ...string) core.Result {
- if !stdlibAssertEqual("wails3", command) {
- t.Fatalf("want %v, got %v", "wails3", command)
- }
- if !stdlibAssertContains(args, "-tags") {
- t.Fatalf("expected %v to contain %v", args, "-tags")
- }
-
- tagIndex := -1
- for i, arg := range args {
- if arg == "-tags" {
- tagIndex = i + 1
- break
- }
- }
- if tagIndex < 1 {
- t.Fatalf("expected %v to be greater than or equal to %v", tagIndex, 1)
- }
- if !stdlibAssertEqual("integration,mlx", args[tagIndex]) {
- t.Fatalf("want %v, got %v", "integration,mlx", args[tagIndex])
- }
-
- return core.Ok("")
- }
-
- result := BuildWailsApp(context.Background(), WailsBuildConfig{
- ProjectDir: projectDir,
- Name: "Core",
- Arch: "arm64",
- BuildTags: []string{"integration"},
- })
- bundle := requireAppleString(t, result)
- if !stdlibAssertEqual(bundlePath, bundle) {
- t.Fatalf("want %v, got %v", bundlePath, bundle)
- }
-
-}
-
-func TestApple_BuildWailsApp_PreBuildsFrontendAndForcesCGO_Good(t *testing.T) {
- projectDir := t.TempDir()
- frontendDir := ax.Join(projectDir, "frontend")
- bundlePath := ax.Join(projectDir, "build", "bin", "Core.app")
- result := storage.Local.EnsureDir(frontendDir)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- oldResolve := appleResolveCommand
- oldCombined := appleCombinedOutput
- t.Cleanup(func() {
- appleResolveCommand = oldResolve
- appleCombinedOutput = oldCombined
- })
-
- var calls []struct {
- dir string
- command string
- args []string
- env []string
- }
-
- appleResolveCommand = func(name string, fallbackPaths ...string) core.Result {
- return core.Ok(name)
- }
- appleCombinedOutput = func(ctx context.Context, dir string, env []string, command string, args ...string) core.Result {
- calls = append(calls, struct {
- dir string
- command string
- args []string
- env []string
- }{
- dir: dir,
- command: command,
- args: append([]string{}, args...),
- env: append([]string{}, env...),
- })
-
- switch command {
- case "deno-build":
- if !stdlibAssertEqual(frontendDir, dir) {
- t.Fatalf("want %v, got %v", frontendDir, dir)
- }
- if !stdlibAssertEqual([]string{"--target", "release"}, args) {
- t.Fatalf("want %v, got %v", []string{"--target", "release"}, args)
- }
-
- case "wails3":
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
- if !stdlibAssertContains(env, "CGO_ENABLED=1") {
- t.Fatalf("expected %v to contain %v", env, "CGO_ENABLED=1")
- }
-
- writeDummyAppBundle(t, bundlePath, "Core", "built")
- default:
- t.Fatalf("unexpected command: %s", command)
- }
-
- return core.Ok("")
- }
-
- result = BuildWailsApp(context.Background(), WailsBuildConfig{
- ProjectDir: projectDir,
- Name: "Core",
- Arch: "arm64",
- OutputDir: ax.Join(projectDir, "dist"),
- DenoBuild: "deno-build --target release",
- })
- bundle := requireAppleString(t, result)
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", "Core.app"), bundle) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", "Core.app"), bundle)
- }
- if len(calls) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(calls))
- }
- if !stdlibAssertEqual("deno-build", calls[0].command) {
- t.Fatalf("want %v, got %v", "deno-build", calls[0].command)
- }
- if !stdlibAssertEqual("wails3", calls[1].command) {
- t.Fatalf("want %v, got %v", "wails3", calls[1].command)
- }
-
-}
-
-func TestApple_BuildWailsApp_UsesDenoWhenEnabledWithoutManifest_Good(t *testing.T) {
- projectDir := t.TempDir()
- bundlePath := ax.Join(projectDir, "build", "bin", "Core.app")
- result := ax.WriteFile(ax.Join(projectDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("DENO_ENABLE", "true")
-
- oldResolve := appleResolveCommand
- oldCombined := appleCombinedOutput
- t.Cleanup(func() {
- appleResolveCommand = oldResolve
- appleCombinedOutput = oldCombined
- })
-
- var calls []struct {
- dir string
- command string
- args []string
- }
-
- appleResolveCommand = func(name string, fallbackPaths ...string) core.Result {
- return core.Ok(name)
- }
- appleCombinedOutput = func(ctx context.Context, dir string, env []string, command string, args ...string) core.Result {
- calls = append(calls, struct {
- dir string
- command string
- args []string
- }{
- dir: dir,
- command: command,
- args: append([]string{}, args...),
- })
-
- switch command {
- case "deno":
- if !stdlibAssertEqual(projectDir, dir) {
- t.Fatalf("want %v, got %v", projectDir, dir)
- }
- if !stdlibAssertEqual([]string{"task", "build"}, args) {
- t.Fatalf("want %v, got %v", []string{"task", "build"}, args)
- }
-
- case "wails3":
- writeDummyAppBundle(t, bundlePath, "Core", "built")
- default:
- t.Fatalf("unexpected command: %s", command)
- }
-
- return core.Ok("")
- }
-
- result = BuildWailsApp(context.Background(), WailsBuildConfig{
- ProjectDir: projectDir,
- Name: "Core",
- Arch: "arm64",
- })
- bundle := requireAppleString(t, result)
- if !stdlibAssertEqual(bundlePath, bundle) {
- t.Fatalf("want %v, got %v", bundlePath, bundle)
- }
- if len(calls) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(calls))
- }
- if !stdlibAssertEqual("deno", calls[0].command) {
- t.Fatalf("want %v, got %v", "deno", calls[0].command)
- }
- if !stdlibAssertEqual("wails3", calls[1].command) {
- t.Fatalf("want %v, got %v", "wails3", calls[1].command)
- }
-
-}
-
-func TestApple_BuildApple_Good(t *testing.T) {
- projectDir := t.TempDir()
- outputDir := ax.Join(projectDir, "dist", "apple")
-
- oldBuildWails := appleBuildWailsAppFn
- oldUniversal := appleCreateUniversalFn
- oldSign := appleSignFn
- oldNotarise := appleNotariseFn
- oldDMG := appleCreateDMGFn
- t.Cleanup(func() {
- appleBuildWailsAppFn = oldBuildWails
- appleCreateUniversalFn = oldUniversal
- appleSignFn = oldSign
- appleNotariseFn = oldNotarise
- appleCreateDMGFn = oldDMG
- })
-
- var builtArches []string
- var buildEnvs [][]string
- appleBuildWailsAppFn = func(ctx context.Context, cfg WailsBuildConfig) core.Result {
- builtArches = append(builtArches, cfg.Arch)
- buildEnvs = append(buildEnvs, append([]string{}, cfg.Env...))
- appPath := ax.Join(cfg.OutputDir, cfg.Name+".app")
- writeDummyAppBundle(t, appPath, cfg.Name, cfg.Arch)
- return core.Ok(appPath)
- }
- appleCreateUniversalFn = func(arm64Path, amd64Path, outputPath string) core.Result {
- result := copyPath(storage.Local, arm64Path, outputPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return ax.WriteFile(ax.Join(outputPath, "Contents", "MacOS", "Core"), []byte("universal"), 0o755)
- }
-
- var signCalls []SignConfig
- appleSignFn = func(ctx context.Context, cfg SignConfig) core.Result {
- signCalls = append(signCalls, cfg)
- return core.Ok(nil)
- }
-
- var notarisedPath string
- appleNotariseFn = func(ctx context.Context, cfg NotariseConfig) core.Result {
- notarisedPath = cfg.AppPath
- return core.Ok(nil)
- }
-
- var dmgCall DMGConfig
- appleCreateDMGFn = func(ctx context.Context, cfg DMGConfig) core.Result {
- dmgCall = cfg
- return ax.WriteFile(cfg.OutputPath, []byte("dmg"), 0o644)
- }
-
- buildResult := requireAppleBuildResult(t, BuildApple(context.Background(), &Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "Core",
- Version: "v1.2.3",
- BuildTags: []string{"integration"},
- LDFlags: []string{"-s", "-w"},
- Cache: CacheConfig{
- Enabled: true,
- Paths: []string{
- ax.Join(outputDir, "cache", "go-build"),
- ax.Join(outputDir, "cache", "go-mod"),
- },
- },
- }, AppleOptions{
- BundleID: "ai.lthn.core",
- Arch: "universal",
- Sign: true,
- Notarise: true,
- DMG: true,
- CertIdentity: "Developer ID Application: Lethean CIC (ABC123DEF4)",
- TeamID: "ABC123DEF4",
- AppleID: "dev@example.com",
- Password: "app-password",
- }, "42"))
- if !stdlibAssertEqual([]string{"arm64", "amd64"}, builtArches) {
- t.Fatalf("want %v, got %v", []string{"arm64", "amd64"}, builtArches)
- }
- if len(buildEnvs) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(buildEnvs))
- }
- if !stdlibAssertContains(buildEnvs[0], "GOCACHE="+ax.Join(outputDir, "cache", "go-build")) {
- t.Fatalf("expected %v to contain %v", buildEnvs[0], "GOCACHE="+ax.Join(outputDir, "cache", "go-build"))
- }
- if !stdlibAssertContains(buildEnvs[0], "GOMODCACHE="+ax.Join(outputDir, "cache", "go-mod")) {
- t.Fatalf("expected %v to contain %v", buildEnvs[0], "GOMODCACHE="+ax.Join(outputDir, "cache", "go-mod"))
- }
- if !stdlibAssertEqual(ax.Join(outputDir, "Core.app"), buildResult.BundlePath) {
- t.Fatalf("want %v, got %v", ax.Join(outputDir, "Core.app"), buildResult.BundlePath)
- }
- if !stdlibAssertEqual(ax.Join(outputDir, "Core-1.2.3.dmg"), buildResult.DMGPath) {
- t.Fatalf("want %v, got %v", ax.Join(outputDir, "Core-1.2.3.dmg"), buildResult.DMGPath)
- }
- if !stdlibAssertEqual(buildResult.DMGPath, notarisedPath) {
- t.Fatalf("want %v, got %v", buildResult.DMGPath, notarisedPath)
- }
- if len(signCalls) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(signCalls))
- }
- if !stdlibAssertEqual(buildResult.BundlePath, signCalls[0].AppPath) {
- t.Fatalf("want %v, got %v", buildResult.BundlePath, signCalls[0].AppPath)
- }
- if !stdlibAssertEqual(buildResult.EntitlementsPath, signCalls[0].Entitlements) {
- t.Fatalf("want %v, got %v", buildResult.EntitlementsPath, signCalls[0].Entitlements)
- }
- if !stdlibAssertEqual(buildResult.DMGPath, signCalls[1].AppPath) {
- t.Fatalf("want %v, got %v", buildResult.DMGPath, signCalls[1].AppPath)
- }
- if !stdlibAssertEmpty(signCalls[1].Entitlements) {
- t.Fatalf("expected empty, got %v", signCalls[1].Entitlements)
- }
- if signCalls[1].Hardened {
- t.Fatal("expected false")
- }
- if !stdlibAssertEqual(buildResult.DMGPath, dmgCall.OutputPath) {
- t.Fatalf("want %v, got %v", buildResult.DMGPath, dmgCall.OutputPath)
- }
-
- plistContent := requireAppleString(t, storage.Local.Read(buildResult.InfoPlistPath))
- if !stdlibAssertContains(plistContent, "ai.lthn.core") {
- t.Fatalf("expected %v to contain %v", plistContent, "ai.lthn.core")
- }
- if !stdlibAssertContains(plistContent, "42") {
- t.Fatalf("expected %v to contain %v", plistContent, "42")
- }
-
- entitlementsContent := requireAppleString(t, storage.Local.Read(buildResult.EntitlementsPath))
- if !stdlibAssertContains(entitlementsContent, "com.apple.security.app-sandbox") {
- t.Fatalf("expected %v to contain %v", entitlementsContent, "com.apple.security.app-sandbox")
- }
- if !stdlibAssertContains(entitlementsContent, "") {
- t.Fatalf("expected %v to contain %v", entitlementsContent, "")
- }
-
-}
-
-func TestApple_NotariseAuthArgsGood(t *testing.T) {
- args := requireAppleStrings(t, notariseAuthArgs(NotariseConfig{
- APIKeyID: "KEY123",
- APIKeyIssuerID: "ISSUER456",
- APIKeyPath: "/tmp/AuthKey_KEY123.p8",
- }))
- if !stdlibAssertEqual([]string{"--key", "/tmp/AuthKey_KEY123.p8", "--key-id", "KEY123", "--issuer", "ISSUER456"}, args) {
- t.Fatalf("want %v, got %v", []string{"--key", "/tmp/AuthKey_KEY123.p8", "--key-id", "KEY123", "--issuer", "ISSUER456"}, args)
- }
-
- args = requireAppleStrings(t, notariseAuthArgs(NotariseConfig{
- TeamID: "ABC123DEF4",
- AppleID: "dev@example.com",
- Password: "app-password",
- }))
- if !stdlibAssertEqual([]string{"--apple-id", "dev@example.com", "--password", "app-password", "--team-id", "ABC123DEF4"}, args) {
- t.Fatalf("want %v, got %v", []string{"--apple-id", "dev@example.com", "--password", "app-password", "--team-id", "ABC123DEF4"}, args)
- }
-
-}
-
-func TestApple_Notarise_AppendsNotaryLogOnRejectedStatus_Bad(t *testing.T) {
- oldResolve := appleResolveCommand
- oldCombined := appleCombinedOutput
- t.Cleanup(func() {
- appleResolveCommand = oldResolve
- appleCombinedOutput = oldCombined
- })
-
- appleResolveCommand = func(name string, fallbackPaths ...string) core.Result {
- return core.Ok(name)
- }
- appleCombinedOutput = func(ctx context.Context, dir string, env []string, command string, args ...string) core.Result {
- switch command {
- case "ditto":
- return core.Ok("")
- case "xcrun":
- if len(args) < 2 {
- t.Fatalf("expected %v to be greater than or equal to %v", len(args), 2)
- }
- if !stdlibAssertEqual("notarytool", args[0]) {
- t.Fatalf("want %v, got %v", "notarytool", args[0])
- }
-
- switch args[1] {
- case "submit":
- return core.Ok(`{"id":"request-123","status":"Invalid"}`)
- case notaryToolLogCommand:
- return core.Ok("notary log details")
- default:
- t.Fatalf("unexpected xcrun invocation: %v", args)
- }
- default:
- t.Fatalf("unexpected command: %s", command)
- }
-
- return core.Ok("")
- }
-
- result := Notarise(context.Background(), NotariseConfig{
- AppPath: ax.Join(t.TempDir(), "Core.app"),
- APIKeyID: "KEY123",
- APIKeyIssuerID: "ISSUER456",
- APIKeyPath: "/tmp/AuthKey_KEY123.p8",
- })
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "status Invalid") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "status Invalid")
- }
- if !stdlibAssertContains(result.Error(), "notary log details") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "notary log details")
- }
-
-}
-
-func TestApple_BuildApple_AppStorePreflight_Bad(t *testing.T) {
- result := BuildApple(context.Background(), &Config{
- FS: storage.Local,
- ProjectDir: t.TempDir(),
- OutputDir: ax.Join(t.TempDir(), "dist", "apple"),
- Name: "Core",
- Version: "v1.2.3",
- }, AppleOptions{
- BundleID: "ai.lthn.core",
- Arch: "arm64",
- Sign: true,
- AppStore: true,
- CertIdentity: "Developer ID Application: Lethean CIC (ABC123DEF4)",
- APIKeyID: "KEY123",
- APIKeyIssuerID: "ISSUER456",
- APIKeyPath: "/tmp/AuthKey_KEY123.p8",
- ProfilePath: "/tmp/Core.provisionprofile",
- Category: "public.app-category.developer-tools",
- Copyright: "Copyright 2026 Lethean CIC. EUPL-1.2.",
- }, "42")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "distribution certificate") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "distribution certificate")
- }
-
-}
-
-func TestApple_BuildApple_TestFlightRequiresDistributionCertificate_Bad(t *testing.T) {
- result := BuildApple(context.Background(), &Config{
- FS: storage.Local,
- ProjectDir: t.TempDir(),
- OutputDir: ax.Join(t.TempDir(), "dist", "apple"),
- Name: "Core",
- Version: "v1.2.3",
- }, AppleOptions{
- BundleID: "ai.lthn.core",
- Arch: "arm64",
- Sign: true,
- TestFlight: true,
- CertIdentity: "Developer ID Application: Lethean CIC (ABC123DEF4)",
- APIKeyID: "KEY123",
- APIKeyIssuerID: "ISSUER456",
- APIKeyPath: "/tmp/AuthKey_KEY123.p8",
- ProfilePath: "/tmp/Core.provisionprofile",
- }, "42")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "distribution certificate") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "distribution certificate")
- }
-
-}
-
-func TestApple_BuildApple_AppStorePreflight_Good(t *testing.T) {
- projectDir := t.TempDir()
- outputDir := ax.Join(projectDir, "dist", "apple")
- profilePath := ax.Join(projectDir, "Core.provisionprofile")
- result := ax.WriteFile(profilePath, []byte("profile"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- metadataPath := writeAppStoreMetadata(t, projectDir)
-
- oldBuildWails := appleBuildWailsAppFn
- oldSign := appleSignFn
- oldSubmit := appleSubmitAppStoreFn
- t.Cleanup(func() {
- appleBuildWailsAppFn = oldBuildWails
- appleSignFn = oldSign
- appleSubmitAppStoreFn = oldSubmit
- })
-
- appleBuildWailsAppFn = func(ctx context.Context, cfg WailsBuildConfig) core.Result {
- appPath := ax.Join(cfg.OutputDir, cfg.Name+".app")
- writeDummyAppBundle(t, appPath, cfg.Name, "safe")
- return core.Ok(appPath)
- }
- appleSignFn = func(ctx context.Context, cfg SignConfig) core.Result {
- return core.Ok(nil)
- }
-
- var submitCfg AppStoreConfig
- var submitCalled bool
- appleSubmitAppStoreFn = func(ctx context.Context, cfg AppStoreConfig) core.Result {
- submitCalled = true
- submitCfg = cfg
- return core.Ok(nil)
- }
-
- buildResult := requireAppleBuildResult(t, BuildApple(context.Background(), &Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "Core",
- Version: "v1.2.3",
- }, AppleOptions{
- BundleID: "ai.lthn.core",
- Arch: "arm64",
- Sign: true,
- AppStore: true,
- CertIdentity: "Apple Distribution: Lethean CIC (ABC123DEF4)",
- APIKeyID: "KEY123",
- APIKeyIssuerID: "ISSUER456",
- APIKeyPath: "/tmp/AuthKey_KEY123.p8",
- ProfilePath: profilePath,
- MetadataPath: metadataPath,
- PrivacyPolicyURL: "https://lthn.ai/privacy",
- Category: "public.app-category.developer-tools",
- Copyright: "Copyright 2026 Lethean CIC. EUPL-1.2.",
- }, "42"))
- if stdlibAssertNil(buildResult) {
- t.Fatal("expected non-nil")
- }
- if !(submitCalled) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(buildResult.BundlePath, submitCfg.AppPath) {
- t.Fatalf("want %v, got %v", buildResult.BundlePath, submitCfg.AppPath)
- }
- if !stdlibAssertEqual("1.2.3", submitCfg.Version) {
- t.Fatalf("want %v, got %v", "1.2.3", submitCfg.Version)
- }
- if !stdlibAssertEqual("manual", submitCfg.ReleaseType) {
- t.Fatalf("want %v, got %v", "manual", submitCfg.ReleaseType)
- }
-
-}
-
-func TestApple_ValidatePrivacyPolicyURLBad(t *testing.T) {
- result := validatePrivacyPolicyURL("")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "privacy_policy_url") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "privacy_policy_url")
- }
-
- result = validatePrivacyPolicyURL("https://example.com")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "non-root path") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "non-root path")
- }
-
-}
-
-func TestApple_ValidateAppStoreMetadataBad(t *testing.T) {
- projectDir := t.TempDir()
- metadataPath := ax.Join(projectDir, ".core", "apple", "appstore")
- result := storage.Local.EnsureDir(ax.Join(metadataPath, "screenshots"))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(metadataPath, "screenshots", "shot.png"), []byte("png"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- result = validateAppStoreMetadata(storage.Local, projectDir, metadataPath)
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "description") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "description")
- }
-
-}
-
-func TestApple_ScanBundleForPrivateAPIUsageBad(t *testing.T) {
- appPath := ax.Join(t.TempDir(), "Core.app")
- writeDummyAppBundle(t, appPath, "Core", "/System/Library/PrivateFrameworks/Example.framework")
-
- result := scanBundleForPrivateAPIUsage(storage.Local, appPath)
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "private API usage detected") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "private API usage detected")
- }
-
-}
-
-func TestApple_UploadTestFlight_Bad(t *testing.T) {
- result := UploadTestFlight(context.Background(), TestFlightConfig{
- AppPath: "build/Core.app",
- APIKeyID: "KEY123",
- APIKeyIssuerID: "ISSUER456",
- })
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "api_key_path") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "api_key_path")
- }
-
-}
-
-func TestApple_SubmitAppStore_Bad(t *testing.T) {
- result := SubmitAppStore(context.Background(), AppStoreConfig{
- AppPath: "build/Core.app",
- APIKeyID: "KEY123",
- APIKeyIssuerID: "ISSUER456",
- })
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "api_key_path") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "api_key_path")
- }
-
-}
-
-func TestApple_PackageForASCUpload_StagesAPIKeyWithCanonicalNameGood(t *testing.T) {
- keyPath := ax.Join(t.TempDir(), "lethean-app-store-key.p8")
- result := ax.WriteFile(keyPath, []byte("private-key"), 0o600)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- pkgPath := ax.Join(t.TempDir(), "Core.pkg")
-
- uploadPackage := requireAppleASCPackage(t, packageForASCUpload(context.Background(), pkgPath, "", "KEY123", keyPath))
- if stdlibAssertNil(uploadPackage.cleanup) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(pkgPath, uploadPackage.path) {
- t.Fatalf("want %v, got %v", pkgPath, uploadPackage.path)
- }
- if len(uploadPackage.env) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(uploadPackage.env))
- }
-
- stagedDir := envDirValue(t, uploadPackage.env, "API_PRIVATE_KEYS_DIR")
- stagedPath := ax.Join(stagedDir, "AuthKey_KEY123.p8")
- content := requireAppleString(t, storage.Local.Read(stagedPath))
- if !stdlibAssertEqual("private-key", content) {
- t.Fatalf("want %v, got %v", "private-key", content)
- }
-
- uploadPackage.cleanup()
- if storage.Local.Exists(stagedDir) {
- t.Fatal("expected false")
- }
-
-}
-
-func TestApple_PackageForASCUpload_UsesExistingCanonicalKeyPathGood(t *testing.T) {
- keyDir := t.TempDir()
- keyPath := ax.Join(keyDir, "AuthKey_KEY123.p8")
- result := ax.WriteFile(keyPath, []byte("private-key"), 0o600)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- pkgPath := ax.Join(t.TempDir(), "Core.pkg")
-
- uploadPackage := requireAppleASCPackage(t, packageForASCUpload(context.Background(), pkgPath, "", "KEY123", keyPath))
- if stdlibAssertNil(uploadPackage.cleanup) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(pkgPath, uploadPackage.path) {
- t.Fatalf("want %v, got %v", pkgPath, uploadPackage.path)
- }
- if len(uploadPackage.env) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(uploadPackage.env))
- }
- if !stdlibAssertEqual(keyDir, envDirValue(t, uploadPackage.env, "API_PRIVATE_KEYS_DIR")) {
- t.Fatalf("want %v, got %v", keyDir, envDirValue(t, uploadPackage.env, "API_PRIVATE_KEYS_DIR"))
- }
-
- uploadPackage.cleanup()
- if !(storage.Local.Exists(keyDir)) {
- t.Fatal("expected true")
- }
- if !(storage.Local.Exists(keyPath)) {
- t.Fatal("expected true")
- }
-
-}
-
-func writeDummyAppBundle(t *testing.T, appPath, executableName, marker string) {
- t.Helper()
- result := storage.Local.EnsureDir(ax.Join(appPath, "Contents", "MacOS"))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- result = WriteInfoPlist(storage.Local, appPath, InfoPlist{
- BundleID: "ai.lthn.core",
- BundleName: executableName,
- BundleDisplayName: executableName,
- BundleVersion: "1.0.0",
- BuildNumber: "1",
- MinSystemVersion: "13.0",
- Category: "public.app-category.developer-tools",
- Executable: executableName,
- HighResCapable: true,
- SupportsSecureRestorableState: true,
- })
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(appPath, "Contents", "MacOS", executableName), []byte(marker), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func writeDummyExecutable(t *testing.T, path, marker string) {
- t.Helper()
- result := storage.Local.EnsureDir(ax.Dir(path))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(path, []byte(marker), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func writeAppStoreMetadata(t *testing.T, projectDir string) string {
- t.Helper()
-
- metadataPath := ax.Join(projectDir, ".core", "apple", "appstore")
- result := storage.Local.EnsureDir(ax.Join(metadataPath, "screenshots"))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(metadataPath, "description.txt"), []byte("Core App Store description"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(metadataPath, "screenshots", "shot-1.png"), []byte("png"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return metadataPath
-}
-
-func envDirValue(t *testing.T, env []string, key string) string {
- t.Helper()
-
- prefix := key + "="
- for _, entry := range env {
- if value, ok := assertEnvEntry(entry, prefix); ok {
- return value
- }
- }
-
- t.Fatalf("environment variable %s not found", key)
- return ""
-}
-
-func assertEnvEntry(entry, prefix string) (string, bool) {
- if len(entry) <= len(prefix) || entry[:len(prefix)] != prefix {
- return "", false
- }
- return entry[len(prefix):], true
-}
-
-func indexOf(values []string, needle string) int {
- for i, value := range values {
- if value == needle {
- return i
- }
- }
- return -1
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestApple_DefaultAppleOptions_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultAppleOptions()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_DefaultAppleOptions_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultAppleOptions()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_DefaultAppleOptions_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultAppleOptions()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_AppleConfig_Resolve_Good(t *core.T) {
- subject := AppleConfig{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Resolve()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleConfig_Resolve_Bad(t *core.T) {
- subject := AppleConfig{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Resolve()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleConfig_Resolve_Ugly(t *core.T) {
- subject := AppleConfig{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Resolve()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_BuildApple_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = BuildApple(ctx, nil, AppleOptions{}, "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_BuildApple_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = BuildApple(ctx, &Config{}, AppleOptions{}, "agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_BuildWailsApp_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = BuildWailsApp(ctx, WailsBuildConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_BuildWailsApp_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = BuildWailsApp(ctx, WailsBuildConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_BuildWailsApp_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = BuildWailsApp(ctx, WailsBuildConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_CreateUniversal_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateUniversal("", "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_CreateUniversal_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateUniversal(core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_Sign_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Sign(ctx, SignConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_Sign_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Sign(ctx, SignConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_Sign_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Sign(ctx, SignConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_Notarise_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Notarise(ctx, NotariseConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_Notarise_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Notarise(ctx, NotariseConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_Notarise_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Notarise(ctx, NotariseConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_CreateDMG_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateDMG(ctx, DMGConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_CreateDMG_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateDMG(ctx, DMGConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_CreateDMG_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CreateDMG(ctx, DMGConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_UploadTestFlight_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = UploadTestFlight(ctx, TestFlightConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_UploadTestFlight_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = UploadTestFlight(ctx, TestFlightConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_SubmitAppStore_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = SubmitAppStore(ctx, AppStoreConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_SubmitAppStore_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = SubmitAppStore(ctx, AppStoreConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WriteInfoPlist_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteInfoPlist(storage.NewMemoryMedium(), "", InfoPlist{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WriteInfoPlist_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteInfoPlist(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), InfoPlist{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WriteEntitlements_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteEntitlements(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), Entitlements{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WriteEntitlements_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteEntitlements(storage.NewMemoryMedium(), "", Entitlements{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WriteEntitlements_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteEntitlements(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), Entitlements{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_InfoPlist_Values_Good(t *core.T) {
- subject := InfoPlist{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Values()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_InfoPlist_Values_Bad(t *core.T) {
- subject := InfoPlist{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Values()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_InfoPlist_Values_Ugly(t *core.T) {
- subject := InfoPlist{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Values()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_Entitlements_Values_Good(t *core.T) {
- subject := Entitlements{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Values()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_Entitlements_Values_Bad(t *core.T) {
- subject := Entitlements{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Values()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_Entitlements_Values_Ugly(t *core.T) {
- subject := Entitlements{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Values()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/archive.go b/pkg/build/archive.go
deleted file mode 100644
index 3a3f6d3..0000000
--- a/pkg/build/archive.go
+++ /dev/null
@@ -1,397 +0,0 @@
-// Package build provides project type detection and cross-compilation for the Core build system.
-package build
-
-import (
- "archive/tar"
- "archive/zip"
- "compress/gzip"
- stdio "io"
- stdfs "io/fs"
- "slices"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- io_interface "dappco.re/go/build/pkg/storage"
- "github.com/Snider/Borg/pkg/compress"
-)
-
-// ArchiveFormat specifies the compression format for archives.
-//
-// var fmt build.ArchiveFormat = build.ArchiveFormatGzip
-type ArchiveFormat string
-
-const (
- // ArchiveFormatGzip uses tar.gz (gzip compression) - widely compatible.
- ArchiveFormatGzip ArchiveFormat = "gz"
- // ArchiveFormatXZ uses tar.xz (xz/LZMA2 compression) - better compression ratio.
- ArchiveFormatXZ ArchiveFormat = "xz"
- // ArchiveFormatZip uses zip archives on any platform.
- ArchiveFormatZip ArchiveFormat = "zip"
-)
-
-// ParseArchiveFormat converts a user-facing archive format string into an ArchiveFormat.
-//
-// format, err := build.ParseArchiveFormat("xz") // → build.ArchiveFormatXZ
-// format, err := build.ParseArchiveFormat("zip") // → build.ArchiveFormatZip
-func ParseArchiveFormat(value string) core.Result {
- switch core.Trim(core.Lower(value)) {
- case "", "gz", "gzip", "tgz", "tar.gz", "tar-gz":
- return core.Ok(ArchiveFormatGzip)
- case "xz", "txz", "tar.xz", "tar-xz":
- return core.Ok(ArchiveFormatXZ)
- case "zip":
- return core.Ok(ArchiveFormatZip)
- default:
- return core.Fail(core.E("build.ParseArchiveFormat", "unsupported archive format: "+value, nil))
- }
-}
-
-// Archive creates an archive for a single artifact using gzip compression.
-// Uses tar.gz for linux/darwin and zip for windows.
-// The archive is created alongside the binary (e.g., dist/myapp_linux_amd64.tar.gz).
-// Returns a new Artifact with Path pointing to the archive.
-//
-// archived, err := build.Archive(io.Local, artifact)
-func Archive(fs io_interface.Medium, artifact Artifact) core.Result {
- return ArchiveWithFormat(fs, artifact, ArchiveFormatGzip)
-}
-
-// ArchiveXZ creates an archive for a single artifact using xz compression.
-// Uses tar.xz for linux/darwin and zip for windows.
-// Returns a new Artifact with Path pointing to the archive.
-//
-// archived, err := build.ArchiveXZ(io.Local, artifact)
-func ArchiveXZ(fs io_interface.Medium, artifact Artifact) core.Result {
- return ArchiveWithFormat(fs, artifact, ArchiveFormatXZ)
-}
-
-// ArchiveWithFormat creates an archive for a single artifact with the specified format.
-// Uses tar.gz, tar.xz, or zip depending on the requested format.
-// Windows artifacts always use zip unless zip is requested explicitly.
-// The archive is created alongside the binary (e.g., dist/myapp_linux_amd64.tar.xz).
-// Returns a new Artifact with Path pointing to the archive.
-//
-// archived, err := build.ArchiveWithFormat(io.Local, artifact, build.ArchiveFormatXZ)
-func ArchiveWithFormat(fs io_interface.Medium, artifact Artifact, format ArchiveFormat) core.Result {
- if artifact.Path == "" {
- return core.Fail(core.E("build.Archive", "artifact path is empty", nil))
- }
-
- // Verify the source file exists
- if stat := fs.Stat(artifact.Path); !stat.OK {
- return core.Fail(core.E("build.Archive", "source file not found", core.NewError(stat.Error())))
- }
-
- // Determine archive type based on OS and format.
- var archivePath string
- var archiveFunc func(fs io_interface.Medium, src, dst string) core.Result
-
- switch {
- case format == ArchiveFormatZip || artifact.OS == "windows":
- archivePath = archiveFilename(artifact, ".zip")
- archiveFunc = createZipArchive
- case format == ArchiveFormatXZ:
- archivePath = archiveFilename(artifact, ".tar.xz")
- archiveFunc = createTarXzArchive
- default:
- archivePath = archiveFilename(artifact, ".tar.gz")
- archiveFunc = createTarGzArchive
- }
-
- // Create the archive
- archived := archiveFunc(fs, artifact.Path, archivePath)
- if !archived.OK {
- return core.Fail(core.E("build.Archive", "failed to create archive", core.NewError(archived.Error())))
- }
-
- return core.Ok(Artifact{
- Path: archivePath,
- OS: artifact.OS,
- Arch: artifact.Arch,
- Checksum: artifact.Checksum,
- })
-}
-
-// ArchiveAll archives all artifacts using gzip compression.
-// Returns a slice of new artifacts pointing to the archives.
-//
-// archived, err := build.ArchiveAll(io.Local, artifacts)
-func ArchiveAll(fs io_interface.Medium, artifacts []Artifact) core.Result {
- return ArchiveAllWithFormat(fs, artifacts, ArchiveFormatGzip)
-}
-
-// ArchiveAllXZ archives all artifacts using xz compression.
-// Returns a slice of new artifacts pointing to the archives.
-//
-// archived, err := build.ArchiveAllXZ(io.Local, artifacts)
-func ArchiveAllXZ(fs io_interface.Medium, artifacts []Artifact) core.Result {
- return ArchiveAllWithFormat(fs, artifacts, ArchiveFormatXZ)
-}
-
-// ArchiveAllWithFormat archives all artifacts with the specified format.
-// Returns a slice of new artifacts pointing to the archives.
-//
-// archived, err := build.ArchiveAllWithFormat(io.Local, artifacts, build.ArchiveFormatXZ)
-func ArchiveAllWithFormat(fs io_interface.Medium, artifacts []Artifact, format ArchiveFormat) core.Result {
- if len(artifacts) == 0 {
- return core.Ok([]Artifact(nil))
- }
-
- var archived []Artifact
- for _, artifact := range artifacts {
- arch := ArchiveWithFormat(fs, artifact, format)
- if !arch.OK {
- return core.Fail(core.E("build.ArchiveAll", "failed to archive "+artifact.Path, core.NewError(arch.Error())))
- }
- archived = append(archived, arch.Value.(Artifact))
- }
-
- return core.Ok(archived)
-}
-
-// archiveFilename generates the archive filename based on the artifact and extension.
-// Format: dist/myapp_linux_amd64.tar.gz (binary name taken from artifact path).
-func archiveFilename(artifact Artifact, ext string) string {
- // Get the directory containing the binary (e.g., dist/linux_amd64)
- dir := ax.Dir(artifact.Path)
- // Go up one level to the output directory (e.g., dist)
- outputDir := ax.Dir(dir)
-
- // Get the binary or bundle name without packaging extensions.
- binaryName := archiveBaseName(artifact.Path)
- if !archiveBaseNameHasPlatformSuffix(binaryName, artifact.OS, artifact.Arch) {
- binaryName = core.Sprintf("%s_%s_%s", binaryName, artifact.OS, artifact.Arch)
- }
-
- // Construct archive name: myapp_linux_amd64.tar.gz
- archiveName := core.Concat(binaryName, ext)
-
- return ax.Join(outputDir, archiveName)
-}
-
-func archiveBaseName(path string) string {
- name := ax.Base(path)
- name = core.TrimSuffix(name, ".exe")
- name = core.TrimSuffix(name, ".app")
- return name
-}
-
-func archiveBaseNameHasPlatformSuffix(name, os, arch string) bool {
- if name == "" || os == "" || arch == "" {
- return false
- }
-
- platform := core.Sprintf("_%s_%s", os, arch)
- return core.HasSuffix(name, platform) || core.Contains(name, platform+"_")
-}
-
-// createTarXzArchive creates a tar.xz archive containing a file or directory tree.
-func createTarXzArchive(fs io_interface.Medium, src, dst string) core.Result {
- // Create tar archive in memory
- tarBuf := core.NewBuffer()
- tarWriter := tar.NewWriter(tarBuf)
- written := writeTarTree(fs, tarWriter, src, src)
- if !written.OK {
- return written
- }
-
- if err := tarWriter.Close(); err != nil {
- return core.Fail(core.E("build.createTarXzArchive", "failed to close tar writer", err))
- }
-
- // Compress with xz using the external compression library.
- xzData, err := compress.Compress(tarBuf.Bytes(), "xz")
- if err != nil {
- return core.Fail(core.E("build.createTarXzArchive", "failed to compress with xz", err))
- }
-
- return writeArchiveBytes(fs, dst, xzData, "build.createTarXzArchive")
-}
-
-// createTarGzArchive creates a tar.gz archive containing a file or directory tree.
-func createTarGzArchive(fs io_interface.Medium, src, dst string) core.Result {
- buf := core.NewBuffer()
-
- // Create gzip writer
- gzWriter := gzip.NewWriter(buf)
-
- // Create tar writer
- tarWriter := tar.NewWriter(gzWriter)
-
- written := writeTarTree(fs, tarWriter, src, src)
- if !written.OK {
- tarWriter.Close()
- gzWriter.Close()
- return written
- }
- if err := tarWriter.Close(); err != nil {
- return core.Fail(core.E("build.createTarGzArchive", "failed to close tar writer", err))
- }
- if err := gzWriter.Close(); err != nil {
- return core.Fail(core.E("build.createTarGzArchive", "failed to close gzip writer", err))
- }
-
- return writeArchiveBytes(fs, dst, buf.Bytes(), "build.createTarGzArchive")
-}
-
-// createZipArchive creates a zip archive containing a file or directory tree.
-func createZipArchive(fs io_interface.Medium, src, dst string) core.Result {
- buf := core.NewBuffer()
-
- // Create zip writer
- zipWriter := zip.NewWriter(buf)
-
- written := writeZipTree(fs, zipWriter, src, src)
- if !written.OK {
- zipWriter.Close()
- return written
- }
- if err := zipWriter.Close(); err != nil {
- return core.Fail(core.E("build.createZipArchive", "failed to close zip writer", err))
- }
-
- return writeArchiveBytes(fs, dst, buf.Bytes(), "build.createZipArchive")
-}
-
-func writeArchiveBytes(fs io_interface.Medium, dst string, data []byte, operation string) core.Result {
- written := fs.Write(dst, string(data))
- if !written.OK {
- return core.Fail(core.E(operation, "failed to write archive file", core.NewError(written.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func writeTarTree(fs io_interface.Medium, writer *tar.Writer, rootPath, currentPath string) core.Result {
- info := fs.Stat(currentPath)
- if !info.OK {
- return core.Fail(core.E("build.writeTarTree", "failed to stat archive entry", core.NewError(info.Error())))
- }
- fileInfo := info.Value.(stdfs.FileInfo)
-
- header, err := tar.FileInfoHeader(fileInfo, "")
- if err != nil {
- return core.Fail(core.E("build.writeTarTree", "failed to create tar header", err))
- }
- header.Name = archiveEntryName(rootPath, currentPath)
- if fileInfo.IsDir() {
- header.Name += "/"
- }
-
- if err := writer.WriteHeader(header); err != nil {
- return core.Fail(core.E("build.writeTarTree", "failed to write tar header", err))
- }
-
- if fileInfo.IsDir() {
- entries := fs.List(currentPath)
- if !entries.OK {
- return core.Fail(core.E("build.writeTarTree", "failed to list archive directory", core.NewError(entries.Error())))
- }
- dirEntries := entries.Value.([]core.FsDirEntry)
- sortDirEntries(dirEntries)
- for _, entry := range dirEntries {
- written := writeTarTree(fs, writer, rootPath, ax.Join(currentPath, entry.Name()))
- if !written.OK {
- return written
- }
- }
- return core.Ok(nil)
- }
-
- source := fs.Open(currentPath)
- if !source.OK {
- return core.Fail(core.E("build.writeTarTree", "failed to open archive entry", core.NewError(source.Error())))
- }
- stream := source.Value.(core.FsFile)
- defer stream.Close()
-
- if _, err := stdio.Copy(writer, stream); err != nil {
- return core.Fail(core.E("build.writeTarTree", "failed to write file content to tar", err))
- }
-
- return core.Ok(nil)
-}
-
-func writeZipTree(fs io_interface.Medium, writer *zip.Writer, rootPath, currentPath string) core.Result {
- info := fs.Stat(currentPath)
- if !info.OK {
- return core.Fail(core.E("build.writeZipTree", "failed to stat archive entry", core.NewError(info.Error())))
- }
- fileInfo := info.Value.(stdfs.FileInfo)
-
- header, err := zip.FileInfoHeader(fileInfo)
- if err != nil {
- return core.Fail(core.E("build.writeZipTree", "failed to create zip header", err))
- }
- header.Name = archiveEntryName(rootPath, currentPath)
-
- if fileInfo.IsDir() {
- header.Name += "/"
- if _, err := writer.CreateHeader(header); err != nil {
- return core.Fail(core.E("build.writeZipTree", "failed to create zip directory entry", err))
- }
-
- entries := fs.List(currentPath)
- if !entries.OK {
- return core.Fail(core.E("build.writeZipTree", "failed to list archive directory", core.NewError(entries.Error())))
- }
- dirEntries := entries.Value.([]core.FsDirEntry)
- sortDirEntries(dirEntries)
- for _, entry := range dirEntries {
- written := writeZipTree(fs, writer, rootPath, ax.Join(currentPath, entry.Name()))
- if !written.OK {
- return written
- }
- }
- return core.Ok(nil)
- }
-
- header.Method = zip.Deflate
- zipEntry, err := writer.CreateHeader(header)
- if err != nil {
- return core.Fail(core.E("build.writeZipTree", "failed to create zip entry", err))
- }
-
- source := fs.Open(currentPath)
- if !source.OK {
- return core.Fail(core.E("build.writeZipTree", "failed to open archive entry", core.NewError(source.Error())))
- }
- stream := source.Value.(core.FsFile)
- defer func() { _ = stream.Close() }()
-
- if _, err := stdio.Copy(zipEntry, stream); err != nil {
- return core.Fail(core.E("build.writeZipTree", "failed to write file content to zip", err))
- }
-
- return core.Ok(nil)
-}
-
-func archiveEntryName(rootPath, currentPath string) string {
- rootName := ax.Base(rootPath)
- if currentPath == rootPath {
- return rootName
- }
-
- relPathResult := ax.Rel(rootPath, currentPath)
- if !relPathResult.OK {
- return rootName
- }
- relPath := relPathResult.Value.(string)
- if relPath == "" || relPath == "." {
- return rootName
- }
-
- return core.Replace(ax.Join(rootName, relPath), ax.DS(), "/")
-}
-
-func sortDirEntries(entries []stdfs.DirEntry) {
- slices.SortFunc(entries, func(a, b stdfs.DirEntry) int {
- if a.Name() < b.Name() {
- return -1
- }
- if a.Name() > b.Name() {
- return 1
- }
- return 0
- })
-}
diff --git a/pkg/build/archive_example_test.go b/pkg/build/archive_example_test.go
deleted file mode 100644
index abdba76..0000000
--- a/pkg/build/archive_example_test.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleParseArchiveFormat references ParseArchiveFormat on this package API surface.
-func ExampleParseArchiveFormat() {
- _ = ParseArchiveFormat
- core.Println("ParseArchiveFormat")
- // Output: ParseArchiveFormat
-}
-
-// ExampleArchive references Archive on this package API surface.
-func ExampleArchive() {
- _ = Archive
- core.Println("Archive")
- // Output: Archive
-}
-
-// ExampleArchiveXZ references ArchiveXZ on this package API surface.
-func ExampleArchiveXZ() {
- _ = ArchiveXZ
- core.Println("ArchiveXZ")
- // Output: ArchiveXZ
-}
-
-// ExampleArchiveWithFormat references ArchiveWithFormat on this package API surface.
-func ExampleArchiveWithFormat() {
- _ = ArchiveWithFormat
- core.Println("ArchiveWithFormat")
- // Output: ArchiveWithFormat
-}
-
-// ExampleArchiveAll references ArchiveAll on this package API surface.
-func ExampleArchiveAll() {
- _ = ArchiveAll
- core.Println("ArchiveAll")
- // Output: ArchiveAll
-}
-
-// ExampleArchiveAllXZ references ArchiveAllXZ on this package API surface.
-func ExampleArchiveAllXZ() {
- _ = ArchiveAllXZ
- core.Println("ArchiveAllXZ")
- // Output: ArchiveAllXZ
-}
-
-// ExampleArchiveAllWithFormat references ArchiveAllWithFormat on this package API surface.
-func ExampleArchiveAllWithFormat() {
- _ = ArchiveAllWithFormat
- core.Println("ArchiveAllWithFormat")
- // Output: ArchiveAllWithFormat
-}
diff --git a/pkg/build/archive_test.go b/pkg/build/archive_test.go
deleted file mode 100644
index fc9d10f..0000000
--- a/pkg/build/archive_test.go
+++ /dev/null
@@ -1,1028 +0,0 @@
-package build
-
-import (
- "archive/tar"
- "archive/zip"
- "compress/gzip"
- "io"
- stdfs "io/fs"
- "reflect"
- "testing"
-
- "dappco.re/go/build/internal/ax"
-
- core "dappco.re/go"
- io_interface "dappco.re/go/build/pkg/storage"
- "github.com/Snider/Borg/pkg/compress"
-)
-
-func archiveRequireNoError(t *testing.T, err any) {
- t.Helper()
- switch value := err.(type) {
- case nil:
- return
- case core.Result:
- if !value.OK {
- t.Fatalf("unexpected error: %v", value.Error())
- }
- case error:
- if value != nil {
- t.Fatalf("unexpected error: %v", value)
- }
- default:
- t.Fatalf("unexpected error value: %v", value)
- }
-}
-
-func archiveAssertNoError(t *testing.T, err any) {
- t.Helper()
- archiveRequireNoError(t, err)
-}
-
-func archiveAssertError(t *testing.T, err any) {
- t.Helper()
- switch value := err.(type) {
- case core.Result:
- if value.OK {
- t.Fatal("expected error")
- }
- case error:
- if value == nil {
- t.Fatal("expected error")
- }
- default:
- t.Fatal("expected error")
- }
-}
-
-func archiveResultError(t *testing.T, result core.Result) string {
- t.Helper()
- if result.OK {
- t.Fatal("expected error")
- }
- return result.Error()
-}
-
-func archiveRequireArtifact(t *testing.T, result core.Result) Artifact {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(Artifact)
-}
-
-func archiveRequireArtifacts(t *testing.T, result core.Result) []Artifact {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result.Value == nil {
- return nil
- }
- return result.Value.([]Artifact)
-}
-
-func archiveRequireFormat(t *testing.T, result core.Result) ArchiveFormat {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(ArchiveFormat)
-}
-
-func archiveRequireBytes(t *testing.T, result core.Result) []byte {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]byte)
-}
-
-func archiveRequireFileInfo(t *testing.T, result core.Result) stdfs.FileInfo {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(stdfs.FileInfo)
-}
-
-func archiveRequireFile(t *testing.T, result core.Result) core.FsFile {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(core.FsFile)
-}
-
-func archiveAssertEqual(t *testing.T, want, got any) {
- t.Helper()
- if !stdlibAssertEqual(want, got) {
- t.Fatalf("want %v, got %v", want, got)
- }
-}
-
-func archiveAssertContains(t *testing.T, value, contains any) {
- t.Helper()
- if !stdlibAssertContains(value, contains) {
- t.Fatalf("expected %v to contain %v", value, contains)
- }
-}
-
-func archiveAssertEmpty(t *testing.T, value any) {
- t.Helper()
- if !stdlibAssertEmpty(value) {
- t.Fatalf("expected empty, got %v", value)
- }
-}
-
-func archiveAssertNil(t *testing.T, value any) {
- t.Helper()
- if !stdlibAssertNil(value) {
- t.Fatalf("expected nil, got %v", value)
- }
-}
-
-func archiveAssertFileExists(t *testing.T, path string) {
- t.Helper()
- if result := ax.Stat(path); !result.OK {
- t.Fatalf("expected file to exist: %v", path)
- }
-}
-
-func archiveRequireLen(t *testing.T, value any, want int) {
- t.Helper()
- got := reflect.ValueOf(value).Len()
- if got != want {
- t.Fatalf("want len %v, got %v", want, got)
- }
-}
-
-func archiveAssertLess(t *testing.T, got, want int64) {
- t.Helper()
- if got >= want {
- t.Fatalf("expected %v to be less than %v", got, want)
- }
-}
-
-// setupArchiveTestFile creates a test binary file in a temp directory with the standard structure.
-// Returns the path to the binary and the output directory.
-func setupArchiveTestFile(t *testing.T, name, os_, arch string) (binaryPath string, outputDir string) {
- t.Helper()
-
- outputDir = t.TempDir()
-
- // Create platform directory: dist/os_arch
- platformDir := ax.Join(outputDir, os_+"_"+arch)
- err := ax.MkdirAll(platformDir, 0755)
- archiveRequireNoError(t, err)
-
- // Create test binary
- binaryPath = ax.Join(platformDir, name)
- content := []byte("#!/bin/bash\necho 'Hello, World!'\n")
- err = ax.WriteFile(binaryPath, content, 0755)
- archiveRequireNoError(t, err)
-
- return binaryPath, outputDir
-}
-
-// setupArchiveTestDirectory creates a test directory artifact in a temp directory.
-// Returns the path to the directory artifact and the output directory.
-func setupArchiveTestDirectory(t *testing.T, name, os_, arch string) (artifactPath string, outputDir string) {
- t.Helper()
-
- outputDir = t.TempDir()
- platformDir := ax.Join(outputDir, os_+"_"+arch)
- archiveRequireNoError(t, ax.MkdirAll(platformDir, 0o755))
-
- artifactPath = ax.Join(platformDir, name)
- archiveRequireNoError(t, ax.MkdirAll(ax.Join(artifactPath, "Contents", "MacOS"), 0o755))
- archiveRequireNoError(t, ax.MkdirAll(ax.Join(artifactPath, "Resources"), 0o755))
- archiveRequireNoError(t, ax.WriteFile(ax.Join(artifactPath, "Contents", "MacOS", "core"), []byte("bundle binary"), 0o755))
- archiveRequireNoError(t, ax.WriteFile(ax.Join(artifactPath, "Resources", "config.json"), []byte(`{"ok":true}`), 0o644))
-
- return artifactPath, outputDir
-}
-
-func TestArchive_Archive_Good(t *testing.T) {
- fs := io_interface.Local
- t.Run("creates tar.gz for linux", func(t *testing.T) {
- binaryPath, outputDir := setupArchiveTestFile(t, "myapp", "linux", "amd64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "amd64",
- }
-
- result := archiveRequireArtifact(t, Archive(fs, artifact))
-
- // Verify archive was created
- expectedPath := ax.Join(outputDir, "myapp_linux_amd64.tar.gz")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
-
- // Verify OS and Arch are preserved
- archiveAssertEqual(t, "linux", result.OS)
- archiveAssertEqual(t, "amd64", result.Arch)
-
- // Verify archive content
- verifyTarGzContent(t, result.Path, "myapp")
- })
-
- t.Run("keeps CI-stamped binary names without double-appending the platform", func(t *testing.T) {
- binaryPath, outputDir := setupArchiveTestFile(t, "myapp_linux_amd64_v1.2.3", "linux", "amd64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "amd64",
- }
-
- result := archiveRequireArtifact(t, Archive(fs, artifact))
-
- expectedPath := ax.Join(outputDir, "myapp_linux_amd64_v1.2.3.tar.gz")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
- })
-
- t.Run("creates tar.gz for darwin", func(t *testing.T) {
- binaryPath, outputDir := setupArchiveTestFile(t, "myapp", "darwin", "arm64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "darwin",
- Arch: "arm64",
- }
-
- result := archiveRequireArtifact(t, Archive(fs, artifact))
-
- expectedPath := ax.Join(outputDir, "myapp_darwin_arm64.tar.gz")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
-
- verifyTarGzContent(t, result.Path, "myapp")
- })
-
- t.Run("creates zip for windows", func(t *testing.T) {
- binaryPath, outputDir := setupArchiveTestFile(t, "myapp.exe", "windows", "amd64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "windows",
- Arch: "amd64",
- }
-
- result := archiveRequireArtifact(t, Archive(fs, artifact))
-
- // Windows archives should strip .exe from archive name
- expectedPath := ax.Join(outputDir, "myapp_windows_amd64.zip")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
-
- verifyZipContent(t, result.Path, "myapp.exe")
- })
-
- t.Run("preserves checksum field", func(t *testing.T) {
- binaryPath, _ := setupArchiveTestFile(t, "myapp", "linux", "amd64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "amd64",
- Checksum: "abc123",
- }
-
- result := archiveRequireArtifact(t, Archive(fs, artifact))
- archiveAssertEqual(t, "abc123", result.Checksum)
- })
-
- t.Run("creates tar.xz for linux with ArchiveXZ", func(t *testing.T) {
- binaryPath, outputDir := setupArchiveTestFile(t, "myapp", "linux", "amd64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "amd64",
- }
-
- result := archiveRequireArtifact(t, ArchiveXZ(fs, artifact))
-
- expectedPath := ax.Join(outputDir, "myapp_linux_amd64.tar.xz")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
-
- verifyTarXzContent(t, result.Path, "myapp")
- })
-
- t.Run("creates tar.xz for darwin with ArchiveWithFormat", func(t *testing.T) {
- binaryPath, outputDir := setupArchiveTestFile(t, "myapp", "darwin", "arm64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "darwin",
- Arch: "arm64",
- }
-
- result := archiveRequireArtifact(t, ArchiveWithFormat(fs, artifact, ArchiveFormatXZ))
-
- expectedPath := ax.Join(outputDir, "myapp_darwin_arm64.tar.xz")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
-
- verifyTarXzContent(t, result.Path, "myapp")
- })
-
- t.Run("windows still uses zip even with xz format", func(t *testing.T) {
- binaryPath, outputDir := setupArchiveTestFile(t, "myapp.exe", "windows", "amd64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "windows",
- Arch: "amd64",
- }
-
- result := archiveRequireArtifact(t, ArchiveWithFormat(fs, artifact, ArchiveFormatXZ))
-
- // Windows should still get .zip regardless of format
- expectedPath := ax.Join(outputDir, "myapp_windows_amd64.zip")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
-
- verifyZipContent(t, result.Path, "myapp.exe")
- })
-
- t.Run("creates zip for linux when explicitly requested", func(t *testing.T) {
- binaryPath, outputDir := setupArchiveTestFile(t, "myapp", "linux", "amd64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "amd64",
- }
-
- result := archiveRequireArtifact(t, ArchiveWithFormat(fs, artifact, ArchiveFormatZip))
-
- expectedPath := ax.Join(outputDir, "myapp_linux_amd64.zip")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
-
- verifyZipContent(t, result.Path, "myapp")
- })
-
- t.Run("creates tar.gz for directory artifacts", func(t *testing.T) {
- artifactPath, outputDir := setupArchiveTestDirectory(t, "Core.app", "darwin", "arm64")
-
- artifact := Artifact{
- Path: artifactPath,
- OS: "darwin",
- Arch: "arm64",
- }
-
- result := archiveRequireArtifact(t, Archive(fs, artifact))
-
- expectedPath := ax.Join(outputDir, "Core_darwin_arm64.tar.gz")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
-
- archiveAssertEqual(t, []byte("bundle binary"), extractTarGzFile(t, result.Path, "Core.app/Contents/MacOS/core"))
- archiveAssertEqual(t, []byte(`{"ok":true}`), extractTarGzFile(t, result.Path, "Core.app/Resources/config.json"))
- })
-
- t.Run("creates zip for directory artifacts", func(t *testing.T) {
- artifactPath, outputDir := setupArchiveTestDirectory(t, "bundle", "linux", "amd64")
-
- artifact := Artifact{
- Path: artifactPath,
- OS: "linux",
- Arch: "amd64",
- }
-
- result := archiveRequireArtifact(t, ArchiveWithFormat(fs, artifact, ArchiveFormatZip))
-
- expectedPath := ax.Join(outputDir, "bundle_linux_amd64.zip")
- archiveAssertEqual(t, expectedPath, result.Path)
- archiveAssertFileExists(t, result.Path)
-
- archiveAssertEqual(t, []byte("bundle binary"), extractZipFile(t, result.Path, "bundle/Contents/MacOS/core"))
- archiveAssertEqual(t, []byte(`{"ok":true}`), extractZipFile(t, result.Path, "bundle/Resources/config.json"))
- })
-}
-
-func TestArchive_ParseArchiveFormat_Good(t *testing.T) {
- t.Run("defaults to gzip when empty", func(t *testing.T) {
- format := archiveRequireFormat(t, ParseArchiveFormat(""))
- archiveAssertEqual(t, ArchiveFormatGzip, format)
- })
-
- t.Run("accepts xz aliases", func(t *testing.T) {
- for _, input := range []string{"xz", "txz", "tar.xz", "tar-xz"} {
- format := archiveRequireFormat(t, ParseArchiveFormat(input))
- archiveAssertEqual(t, ArchiveFormatXZ, format)
- }
- })
-
- t.Run("accepts zip", func(t *testing.T) {
- format := archiveRequireFormat(t, ParseArchiveFormat("zip"))
- archiveAssertEqual(t, ArchiveFormatZip, format)
- })
-
- t.Run("accepts gzip aliases", func(t *testing.T) {
- for _, input := range []string{"gz", "gzip", "tgz", "tar.gz", "tar-gz"} {
- format := archiveRequireFormat(t, ParseArchiveFormat(input))
- archiveAssertEqual(t, ArchiveFormatGzip, format)
- }
- })
-
- t.Run("rejects unsupported formats", func(t *testing.T) {
- result := ParseArchiveFormat("bzip2")
- archiveAssertError(t, result)
- archiveAssertContains(t, result.Error(), "unsupported archive format")
- })
-}
-
-func TestArchive_Archive_Bad(t *testing.T) {
- fs := io_interface.Local
- t.Run("returns error for empty path", func(t *testing.T) {
- artifact := Artifact{
- Path: "",
- OS: "linux",
- Arch: "amd64",
- }
-
- result := Archive(fs, artifact)
- archiveAssertError(t, result)
- archiveAssertContains(t, result.Error(), "artifact path is empty")
- })
-
- t.Run("returns error for non-existent file", func(t *testing.T) {
- artifact := Artifact{
- Path: "/nonexistent/path/binary",
- OS: "linux",
- Arch: "amd64",
- }
-
- result := Archive(fs, artifact)
- archiveAssertError(t, result)
- archiveAssertContains(t, result.Error(), "source file not found")
- })
-
-}
-
-func TestArchive_ArchiveAll_Good(t *testing.T) {
- fs := io_interface.Local
- t.Run("archives multiple artifacts", func(t *testing.T) {
- outputDir := t.TempDir()
-
- // Create multiple binaries
- var artifacts []Artifact
- targets := []struct {
- os_ string
- arch string
- }{
- {"linux", "amd64"},
- {"linux", "arm64"},
- {"darwin", "arm64"},
- {"windows", "amd64"},
- }
-
- for _, target := range targets {
- platformDir := ax.Join(outputDir, target.os_+"_"+target.arch)
- err := ax.MkdirAll(platformDir, 0755)
- archiveRequireNoError(t, err)
-
- name := "myapp"
- if target.os_ == "windows" {
- name = "myapp.exe"
- }
-
- binaryPath := ax.Join(platformDir, name)
- err = ax.WriteFile(binaryPath, []byte("binary content"), 0755)
- archiveRequireNoError(t, err)
-
- artifacts = append(artifacts, Artifact{
- Path: binaryPath,
- OS: target.os_,
- Arch: target.arch,
- })
- }
-
- results := archiveRequireArtifacts(t, ArchiveAll(fs, artifacts))
- archiveRequireLen(t, results, 4)
-
- // Verify all archives were created
- for i, result := range results {
- archiveAssertFileExists(t, result.Path)
- archiveAssertEqual(t, artifacts[i].OS, result.OS)
- archiveAssertEqual(t, artifacts[i].Arch, result.Arch)
- }
- })
-
- t.Run("returns nil for empty slice", func(t *testing.T) {
- results := archiveRequireArtifacts(t, ArchiveAll(fs, []Artifact{}))
- archiveAssertNil(t, results)
- })
-
- t.Run("returns nil for nil slice", func(t *testing.T) {
- results := archiveRequireArtifacts(t, ArchiveAll(fs, nil))
- archiveAssertNil(t, results)
- })
-}
-
-func TestArchive_ArchiveAll_Bad(t *testing.T) {
- fs := io_interface.Local
- t.Run("returns partial results on error", func(t *testing.T) {
- binaryPath, _ := setupArchiveTestFile(t, "myapp", "linux", "amd64")
-
- artifacts := []Artifact{
- {Path: binaryPath, OS: "linux", Arch: "amd64"},
- {Path: "/nonexistent/binary", OS: "linux", Arch: "arm64"}, // This will fail
- }
-
- result := ArchiveAll(fs, artifacts)
- archiveAssertError(t, result)
- archiveAssertContains(t, result.Error(), "failed to archive")
- })
-}
-
-func TestArchive_ArchiveFilenameGood(t *testing.T) {
- t.Run("generates correct tar.gz filename", func(t *testing.T) {
- artifact := Artifact{
- Path: "/output/linux_amd64/myapp",
- OS: "linux",
- Arch: "amd64",
- }
-
- filename := archiveFilename(artifact, ".tar.gz")
- archiveAssertEqual(t, "/output/myapp_linux_amd64.tar.gz", filename)
- })
-
- t.Run("generates correct zip filename", func(t *testing.T) {
- artifact := Artifact{
- Path: "/output/windows_amd64/myapp.exe",
- OS: "windows",
- Arch: "amd64",
- }
-
- filename := archiveFilename(artifact, ".zip")
- archiveAssertEqual(t, "/output/myapp_windows_amd64.zip", filename)
- })
-
- t.Run("handles nested output directories", func(t *testing.T) {
- artifact := Artifact{
- Path: "/project/dist/linux_arm64/cli",
- OS: "linux",
- Arch: "arm64",
- }
-
- filename := archiveFilename(artifact, ".tar.gz")
- archiveAssertEqual(t, "/project/dist/cli_linux_arm64.tar.gz", filename)
- })
-
- t.Run("strips app bundle suffix from archive name", func(t *testing.T) {
- artifact := Artifact{
- Path: "/output/darwin_arm64/Core.app",
- OS: "darwin",
- Arch: "arm64",
- }
-
- filename := archiveFilename(artifact, ".tar.gz")
- archiveAssertEqual(t, "/output/Core_darwin_arm64.tar.gz", filename)
- })
-}
-
-func TestArchive_RoundTripGood(t *testing.T) {
- fs := io_interface.Local
-
- t.Run("tar.gz round trip preserves content", func(t *testing.T) {
- binaryPath, _ := setupArchiveTestFile(t, "roundtrip-app", "linux", "amd64")
-
- // Read original content
- originalContent := archiveRequireBytes(t, ax.ReadFile(binaryPath))
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "amd64",
- }
-
- // Create archive
- archiveArtifact := archiveRequireArtifact(t, Archive(fs, artifact))
- archiveAssertFileExists(t, archiveArtifact.Path)
-
- // Extract and verify content matches
- extractedContent := extractTarGzFile(t, archiveArtifact.Path, "roundtrip-app")
- archiveAssertEqual(t, originalContent, extractedContent)
- })
-
- t.Run("tar.xz round trip preserves content", func(t *testing.T) {
- binaryPath, _ := setupArchiveTestFile(t, "roundtrip-xz", "linux", "arm64")
-
- originalContent := archiveRequireBytes(t, ax.ReadFile(binaryPath))
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "arm64",
- }
-
- archiveArtifact := archiveRequireArtifact(t, ArchiveXZ(fs, artifact))
- archiveAssertFileExists(t, archiveArtifact.Path)
-
- extractedContent := extractTarXzFile(t, archiveArtifact.Path, "roundtrip-xz")
- archiveAssertEqual(t, originalContent, extractedContent)
- })
-
- t.Run("zip round trip preserves content", func(t *testing.T) {
- binaryPath, _ := setupArchiveTestFile(t, "roundtrip.exe", "windows", "amd64")
-
- originalContent := archiveRequireBytes(t, ax.ReadFile(binaryPath))
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "windows",
- Arch: "amd64",
- }
-
- archiveArtifact := archiveRequireArtifact(t, Archive(fs, artifact))
- archiveAssertFileExists(t, archiveArtifact.Path)
-
- extractedContent := extractZipFile(t, archiveArtifact.Path, "roundtrip.exe")
- archiveAssertEqual(t, originalContent, extractedContent)
- })
-
- t.Run("tar.gz preserves file permissions", func(t *testing.T) {
- binaryPath, _ := setupArchiveTestFile(t, "perms-app", "linux", "amd64")
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "amd64",
- }
-
- archiveArtifact := archiveRequireArtifact(t, Archive(fs, artifact))
-
- // Extract and verify permissions are preserved
- mode := extractTarGzFileMode(t, archiveArtifact.Path, "perms-app")
- // The original file was written with 0755
- archiveAssertEqual(t, stdfs.FileMode(0o755), mode&stdfs.ModePerm)
- })
-
- t.Run("round trip with large binary content", func(t *testing.T) {
- outputDir := t.TempDir()
- platformDir := ax.Join(outputDir, "linux_amd64")
- archiveRequireNoError(t, ax.MkdirAll(platformDir, 0755))
-
- // Create a larger file (64KB)
- largeContent := make([]byte, 64*1024)
- for i := range largeContent {
- largeContent[i] = byte(i % 256)
- }
- binaryPath := ax.Join(platformDir, "large-app")
- archiveRequireNoError(t, ax.WriteFile(binaryPath, largeContent, 0755))
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "amd64",
- }
-
- archiveArtifact := archiveRequireArtifact(t, Archive(fs, artifact))
-
- extractedContent := extractTarGzFile(t, archiveArtifact.Path, "large-app")
- archiveAssertEqual(t, largeContent, extractedContent)
- })
-
- t.Run("archive is smaller than original for tar.gz", func(t *testing.T) {
- outputDir := t.TempDir()
- platformDir := ax.Join(outputDir, "linux_amd64")
- archiveRequireNoError(t, ax.MkdirAll(platformDir, 0755))
-
- // Create a compressible file (repeated pattern)
- compressibleContent := make([]byte, 4096)
- for i := range compressibleContent {
- compressibleContent[i] = 'A'
- }
- binaryPath := ax.Join(platformDir, "compressible-app")
- archiveRequireNoError(t, ax.WriteFile(binaryPath, compressibleContent, 0755))
-
- artifact := Artifact{
- Path: binaryPath,
- OS: "linux",
- Arch: "amd64",
- }
-
- archiveArtifact := archiveRequireArtifact(t, Archive(fs, artifact))
-
- originalInfo := archiveRequireFileInfo(t, ax.Stat(binaryPath))
- archiveInfo := archiveRequireFileInfo(t, ax.Stat(archiveArtifact.Path))
-
- // Compressed archive should be smaller than original
- archiveAssertLess(t, archiveInfo.Size(), originalInfo.Size())
- })
-}
-
-// extractTarGzFile extracts a named file from a tar.gz archive and returns its content.
-func extractTarGzFile(t *testing.T, archivePath, fileName string) []byte {
- t.Helper()
-
- file := archiveRequireFile(t, ax.Open(archivePath))
- defer func() { _ = file.Close() }()
-
- gzReader, err := gzip.NewReader(file)
- archiveRequireNoError(t, err)
- defer func() { _ = gzReader.Close() }()
-
- tarReader := tar.NewReader(gzReader)
-
- for {
- header, err := tarReader.Next()
- if err == io.EOF {
- t.Fatalf("file %q not found in archive", fileName)
- }
- archiveRequireNoError(t, err)
-
- if header.Name == fileName {
- content, err := io.ReadAll(tarReader)
- archiveRequireNoError(t, err)
- return content
- }
- }
-}
-
-// extractTarGzFileMode extracts the file mode of a named file from a tar.gz archive.
-func extractTarGzFileMode(t *testing.T, archivePath, fileName string) stdfs.FileMode {
- t.Helper()
-
- file := archiveRequireFile(t, ax.Open(archivePath))
- defer func() { _ = file.Close() }()
-
- gzReader, err := gzip.NewReader(file)
- archiveRequireNoError(t, err)
- defer func() { _ = gzReader.Close() }()
-
- tarReader := tar.NewReader(gzReader)
-
- for {
- header, err := tarReader.Next()
- if err == io.EOF {
- t.Fatalf("file %q not found in archive", fileName)
- }
- archiveRequireNoError(t, err)
-
- if header.Name == fileName {
- return header.FileInfo().Mode()
- }
- }
-}
-
-// extractTarXzFile extracts a named file from a tar.xz archive and returns its content.
-func extractTarXzFile(t *testing.T, archivePath, fileName string) []byte {
- t.Helper()
-
- xzData := archiveRequireBytes(t, ax.ReadFile(archivePath))
-
- tarData, err := compress.Decompress(xzData)
- archiveRequireNoError(t, err)
-
- tarReader := tar.NewReader(core.NewBuffer(tarData))
-
- for {
- header, err := tarReader.Next()
- if err == io.EOF {
- t.Fatalf("file %q not found in archive", fileName)
- }
- archiveRequireNoError(t, err)
-
- if header.Name == fileName {
- content, err := io.ReadAll(tarReader)
- archiveRequireNoError(t, err)
- return content
- }
- }
-}
-
-// extractZipFile extracts a named file from a zip archive and returns its content.
-func extractZipFile(t *testing.T, archivePath, fileName string) []byte {
- t.Helper()
-
- reader, err := zip.OpenReader(archivePath)
- archiveRequireNoError(t, err)
- defer func() { _ = reader.Close() }()
-
- for _, f := range reader.File {
- if f.Name == fileName {
- rc, err := f.Open()
- archiveRequireNoError(t, err)
- defer func() { _ = rc.Close() }()
-
- content, err := io.ReadAll(rc)
- archiveRequireNoError(t, err)
- return content
- }
- }
-
- t.Fatalf("file %q not found in zip archive", fileName)
- return nil
-}
-
-// verifyTarGzContent opens a tar.gz file and verifies it contains the expected file.
-func verifyTarGzContent(t *testing.T, archivePath, expectedName string) {
- t.Helper()
-
- file := archiveRequireFile(t, ax.Open(archivePath))
- defer func() { _ = file.Close() }()
-
- gzReader, err := gzip.NewReader(file)
- archiveRequireNoError(t, err)
- defer func() { _ = gzReader.Close() }()
-
- tarReader := tar.NewReader(gzReader)
-
- header, err := tarReader.Next()
- archiveRequireNoError(t, err)
- archiveAssertEqual(t, expectedName, header.Name)
-
- // Verify there's only one file
- _, err = tarReader.Next()
- archiveAssertEqual(t, io.EOF, err)
-}
-
-// verifyZipContent opens a zip file and verifies it contains the expected file.
-func verifyZipContent(t *testing.T, archivePath, expectedName string) {
- t.Helper()
-
- reader, err := zip.OpenReader(archivePath)
- archiveRequireNoError(t, err)
- defer func() { _ = reader.Close() }()
-
- archiveRequireLen(t, reader.File, 1)
- archiveAssertEqual(t, expectedName, reader.File[0].Name)
-}
-
-// verifyTarXzContent opens a tar.xz file and verifies it contains the expected file.
-func verifyTarXzContent(t *testing.T, archivePath, expectedName string) {
- t.Helper()
-
- // Read the xz-compressed file
- xzData := archiveRequireBytes(t, ax.ReadFile(archivePath))
-
- // Decompress with the deferred Borg API.
- tarData, err := compress.Decompress(xzData)
- archiveRequireNoError(t, err)
-
- // Read tar archive
- tarReader := tar.NewReader(core.NewBuffer(tarData))
-
- header, err := tarReader.Next()
- archiveRequireNoError(t, err)
- archiveAssertEqual(t, expectedName, header.Name)
-
- // Verify there's only one file
- _, err = tarReader.Next()
- archiveAssertEqual(t, io.EOF, err)
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestArchive_ParseArchiveFormat_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ParseArchiveFormat("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestArchive_ParseArchiveFormat_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ParseArchiveFormat("agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestArchive_Archive_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Archive(io_interface.NewMemoryMedium(), Artifact{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestArchive_ArchiveXZ_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveXZ(io_interface.NewMemoryMedium(), Artifact{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestArchive_ArchiveXZ_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveXZ(io_interface.NewMemoryMedium(), Artifact{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestArchive_ArchiveXZ_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveXZ(io_interface.NewMemoryMedium(), Artifact{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestArchive_ArchiveWithFormat_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveWithFormat(io_interface.NewMemoryMedium(), Artifact{}, ArchiveFormat("linux"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestArchive_ArchiveWithFormat_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveWithFormat(io_interface.NewMemoryMedium(), Artifact{}, ArchiveFormat("linux"))
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestArchive_ArchiveWithFormat_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveWithFormat(io_interface.NewMemoryMedium(), Artifact{}, ArchiveFormat("linux"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestArchive_ArchiveAll_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveAll(io_interface.NewMemoryMedium(), nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestArchive_ArchiveAllXZ_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveAllXZ(io_interface.NewMemoryMedium(), nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestArchive_ArchiveAllXZ_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveAllXZ(io_interface.NewMemoryMedium(), nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestArchive_ArchiveAllXZ_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveAllXZ(io_interface.NewMemoryMedium(), nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestArchive_ArchiveAllWithFormat_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveAllWithFormat(io_interface.NewMemoryMedium(), nil, ArchiveFormat("linux"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestArchive_ArchiveAllWithFormat_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveAllWithFormat(io_interface.NewMemoryMedium(), nil, ArchiveFormat("linux"))
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestArchive_ArchiveAllWithFormat_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ArchiveAllWithFormat(io_interface.NewMemoryMedium(), nil, ArchiveFormat("linux"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/build.go b/pkg/build/build.go
deleted file mode 100644
index d7bc379..0000000
--- a/pkg/build/build.go
+++ /dev/null
@@ -1,136 +0,0 @@
-// Package build provides project type detection and cross-compilation for the Core build system.
-// It supports Go, Wails, Node.js, PHP, Python, Rust, Docs, Docker, LinuxKit, C++, and Taskfile
-// projects with automatic detection based on marker files and builder-specific probes.
-package build
-
-import (
- "context"
-
- core "dappco.re/go"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// ProjectType represents a detected project type.
-//
-// var t build.ProjectType = build.ProjectTypeGo
-type ProjectType string
-
-// Project type constants for build detection.
-const (
- // ProjectTypeGo indicates a standard Go project with go.mod or go.work.
- ProjectTypeGo ProjectType = "go"
- // ProjectTypeWails indicates a Wails desktop application.
- ProjectTypeWails ProjectType = "wails"
- // ProjectTypeNode indicates a Node.js project with package.json.
- ProjectTypeNode ProjectType = "node"
- // ProjectTypePHP indicates a PHP/Laravel project with composer.json.
- ProjectTypePHP ProjectType = "php"
- // ProjectTypeCPP indicates a C++ project with CMakeLists.txt.
- ProjectTypeCPP ProjectType = "cpp"
- // ProjectTypeDocker indicates a Docker-based project with Dockerfile.
- ProjectTypeDocker ProjectType = "docker"
- // ProjectTypeLinuxKit indicates a LinuxKit VM configuration.
- ProjectTypeLinuxKit ProjectType = "linuxkit"
- // ProjectTypeTaskfile indicates a project using Taskfile automation.
- ProjectTypeTaskfile ProjectType = "taskfile"
- // ProjectTypeDocs indicates a documentation project with mkdocs.yml.
- ProjectTypeDocs ProjectType = "docs"
- // ProjectTypePython indicates a Python project with pyproject.toml or requirements.txt.
- ProjectTypePython ProjectType = "python"
- // ProjectTypeRust indicates a Rust project with Cargo.toml.
- ProjectTypeRust ProjectType = "rust"
-)
-
-// Target represents a build target platform.
-//
-// t := build.Target{OS: "linux", Arch: "amd64"}
-type Target struct {
- OS string
- Arch string
-}
-
-// String returns the target in GOOS/GOARCH format.
-//
-// s := t.String() // → "linux/amd64"
-func (t Target) String() string {
- return t.OS + "/" + t.Arch
-}
-
-// Artifact represents a build output file.
-//
-// a := build.Artifact{Path: "dist/linux_amd64/myapp", OS: "linux", Arch: "amd64"}
-type Artifact struct {
- Path string
- OS string
- Arch string
- Checksum string
-}
-
-// Config holds build configuration.
-//
-// cfg := &build.Config{FS: storage.Local, ProjectDir: ".", OutputDir: "dist", Name: "myapp"}
-type Config struct {
- // FS is the medium used for file operations.
- FS storage.Medium
- // OutputMedium is the medium used for build artifact output.
- OutputMedium storage.Medium
- // Project holds build-time project metadata.
- Project Project
- // ProjectDir is the root directory of the project.
- ProjectDir string
- // OutputDir is where build artifacts are placed.
- OutputDir string
- // Name is the output binary name.
- Name string
- // Version is the build version string.
- Version string
- // LDFlags are additional linker flags.
- LDFlags []string
- // Flags are additional build flags.
- Flags []string
- // BuildTags are Go build tags passed through to `go build`.
- BuildTags []string
- // Env are additional environment variables.
- Env []string
- // Cache holds build cache configuration for builders that can use it.
- Cache CacheConfig
- // CGO enables CGO for the build (required for Wails, FrankenPHP, etc).
- CGO bool
- // Obfuscate uses garble instead of go build for binary obfuscation.
- Obfuscate bool
- // DenoBuild overrides the default `deno task build` invocation for Deno-backed builds.
- DenoBuild string
- // NpmBuild overrides the default `npm run build` invocation for npm-backed builds.
- NpmBuild string
- // NSIS enables Windows NSIS installer generation (Wails projects only).
- NSIS bool
- // WebView2 sets the WebView2 delivery method: download|embed|browser|error.
- WebView2 string
-
- // Docker-specific config
- Dockerfile string // Path to Dockerfile (default: Dockerfile)
- Registry string // Container registry (default: ghcr.io)
- Image string // Image name (owner/repo format)
- Tags []string // Additional tags to apply
- BuildArgs map[string]string // Docker build arguments
- Push bool // Whether to push after build
- Load bool // Whether to load a single-platform image into the local daemon after build
-
- // LinuxKit-specific config
- LinuxKitConfig string // Path to LinuxKit YAML config, relative to ProjectDir or absolute.
- Formats []string // Output formats (iso, qcow2, raw, vmdk)
- LinuxKit LinuxKitConfig
-}
-
-// Builder defines the interface for project-specific build implementations.
-//
-// var b build.Builder = builders.NewGoBuilder()
-// result := b.Build(ctx, cfg, targets)
-type Builder interface {
- // Name returns the builder's identifier.
- Name() string
- // Detect checks if this builder can handle the project in the given directory.
- Detect(fs storage.Medium, dir string) core.Result
- // Build compiles the project for the specified targets.
- Build(ctx context.Context, cfg *Config, targets []Target) core.Result
-}
diff --git a/pkg/build/build_example_test.go b/pkg/build/build_example_test.go
deleted file mode 100644
index 4ffb271..0000000
--- a/pkg/build/build_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleTarget_String references Target.String on this package API surface.
-func ExampleTarget_String() {
- _ = (*Target).String
- core.Println("Target.String")
- // Output: Target.String
-}
diff --git a/pkg/build/build_test.go b/pkg/build/build_test.go
deleted file mode 100644
index 1aa3d6d..0000000
--- a/pkg/build/build_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-func TestBuild_Target_String_Good(t *core.T) {
- target := Target{OS: "linux", Arch: "amd64"}
- got := target.String()
- core.AssertEqual(t, "linux/amd64", got)
- core.AssertContains(t, got, "linux")
-}
-
-func TestBuild_Target_String_Bad(t *core.T) {
- target := Target{}
- got := target.String()
- core.AssertEqual(t, "/", got)
- core.AssertLen(t, got, 1)
-}
-
-func TestBuild_Target_String_Ugly(t *core.T) {
- target := Target{OS: "darwin", Arch: "arm64/v8"}
- got := target.String()
- core.AssertEqual(t, "darwin/arm64/v8", got)
- core.AssertContains(t, got, "arm64")
-}
diff --git a/pkg/build/builders/apple.go b/pkg/build/builders/apple.go
deleted file mode 100644
index 1ecd498..0000000
--- a/pkg/build/builders/apple.go
+++ /dev/null
@@ -1,627 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- stdio "io"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-const (
- defaultAppleBuilderArch = "universal"
- defaultAppleBuilderMinSystemVersion = "13.0"
- defaultAppleBuilderCategory = "public.app-category.developer-tools"
-)
-
-// Builder aliases the shared build.Builder interface for callers in this package.
-type Builder = build.Builder
-
-// AppleOptions holds the Apple build pipeline settings used by AppleBuilder.
-type AppleOptions struct {
- SigningIdentity string
- CertIdentity string
- BundleID string
- EntitlementsPath string
-
- Arch string
- BundleDisplayName string
- MinSystemVersion string
- Category string
- Copyright string
- BuildNumber string
-
- Sign bool
- Notarise bool
- Notarize bool
- TestFlight bool
- AppStore bool
-
- TeamID string
- AppleID string
- AppPassword string
- Password string
-
- APIKeyID string
- APIKeyIssuerID string
- APIKeyPath string
-
- NotarisationProfile string
- NotarizationProfile string
- NotaryProfile string
-
- TestFlightKeyID string
- TestFlightIssuerID string
- TestFlightKeyPath string
- TestFlightPrivateKey string
- XcodeCloud bool
- DMG AppleDMGConfig
-}
-
-// AppleDMGConfig holds DMG packaging settings for the Apple pipeline.
-type AppleDMGConfig struct {
- Enabled bool
- OutputPath string
- VolumeName string
- BackgroundPath string
- IconSize int
- WindowSize [2]int
-}
-
-// DMGConfig aliases the Apple DMG config for callers that use the shorter name.
-type DMGConfig = AppleDMGConfig
-
-// AppleCommandRunner records or executes an external command invocation.
-type AppleCommandRunner interface {
- Run(ctx context.Context, opts RunOptions) core.Result
-}
-
-// AppleCommandRunnerFunc adapts a function to AppleCommandRunner.
-type AppleCommandRunnerFunc func(ctx context.Context, opts RunOptions) core.Result
-
-// Run implements AppleCommandRunner.
-func (fn AppleCommandRunnerFunc) Run(ctx context.Context, opts RunOptions) core.Result {
- return fn(ctx, opts)
-}
-
-// GoProcessAppleRunner executes commands through Core's process primitive.
-// It is intentionally opt-in because the skeleton defaults to non-executing
-// stubs for sandbox-safe tests.
-type GoProcessAppleRunner struct{}
-
-// Run executes opts through Core's process primitive.
-func (GoProcessAppleRunner) Run(ctx context.Context, opts RunOptions) core.Result {
- return runWithOptions(ctx, opts)
-}
-
-// AppleBuilder implements build.Builder for the Apple build pipeline skeleton.
-type AppleBuilder struct {
- Options AppleOptions
-
- runner AppleCommandRunner
- hostOS string
- todoWriter stdio.Writer
-}
-
-// AppleBuilderOption configures an AppleBuilder.
-type AppleBuilderOption func(*AppleBuilder)
-
-// NewAppleBuilder creates an Apple build pipeline skeleton.
-func NewAppleBuilder(options ...AppleBuilderOption) *AppleBuilder {
- builder := &AppleBuilder{
- Options: DefaultAppleBuilderOptions(),
- hostOS: runtime.GOOS,
- todoWriter: core.Stdout(),
- }
- for _, option := range options {
- if option != nil {
- option(builder)
- }
- }
- return builder
-}
-
-// WithAppleOptions replaces the default Apple options.
-func WithAppleOptions(options AppleOptions) AppleBuilderOption {
- return func(builder *AppleBuilder) {
- builder.Options = options.withDefaults()
- }
-}
-
-// WithAppleCommandRunner configures the command runner used by external stubs.
-func WithAppleCommandRunner(runner AppleCommandRunner) AppleBuilderOption {
- return func(builder *AppleBuilder) {
- builder.runner = runner
- }
-}
-
-// WithAppleHostOS overrides host OS detection, mainly for tests.
-func WithAppleHostOS(hostOS string) AppleBuilderOption {
- return func(builder *AppleBuilder) {
- builder.hostOS = hostOS
- }
-}
-
-// WithAppleTODOWriter configures where structured TODO messages are printed.
-func WithAppleTODOWriter(writer stdio.Writer) AppleBuilderOption {
- return func(builder *AppleBuilder) {
- builder.todoWriter = writer
- }
-}
-
-// DefaultAppleBuilderOptions returns sandbox-safe Apple pipeline defaults.
-func DefaultAppleBuilderOptions() AppleOptions {
- return AppleOptions{
- Arch: defaultAppleBuilderArch,
- MinSystemVersion: defaultAppleBuilderMinSystemVersion,
- Category: defaultAppleBuilderCategory,
- DMG: AppleDMGConfig{
- IconSize: 128,
- WindowSize: [2]int{640, 480},
- },
- }
-}
-
-// Name returns the builder identifier.
-func (b *AppleBuilder) Name() string {
- return "apple"
-}
-
-// Detect checks whether dir looks like a Wails macOS app project.
-func (b *AppleBuilder) Detect(fs coreio.Medium, dir string) core.Result {
- if fs == nil {
- fs = coreio.Local
- }
- return core.Ok(build.IsWailsProject(fs, dir))
-}
-
-// Build runs the Apple build pipeline skeleton.
-func (b *AppleBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("AppleBuilder.Build", "config is nil", nil))
- }
- if ctx == nil {
- ctx = context.Background()
- }
-
- filesystem := ensureBuildFilesystem(cfg)
- artifactFilesystem := build.ResolveOutputMedium(cfg)
- options := b.options()
- valid := ValidateAppleOptions(options)
- if !valid.OK {
- return valid
- }
-
- outputDir := resolveAppleBuilderOutputDir(cfg, artifactFilesystem)
- created := artifactFilesystem.EnsureDir(outputDir)
- if !created.OK {
- return core.Fail(core.E("AppleBuilder.Build", "failed to create Apple output directory", core.NewError(created.Error())))
- }
-
- name := resolveAppleBuilderName(cfg)
- buildNumber := firstNonEmptyApple(options.BuildNumber, "1")
- if options.XcodeCloud {
- written := b.WriteXcodeCloudConfig(artifactFilesystem, cfg.ProjectDir, cfg, options)
- if !written.OK {
- return written
- }
- }
-
- targetArch := resolveAppleBuilderArch(options, targets)
- bundleResult := b.buildBundle(ctx, filesystem, artifactFilesystem, cfg, outputDir, name, targetArch)
- if !bundleResult.OK {
- return bundleResult
- }
- bundlePath := bundleResult.Value.(string)
-
- plist := WriteAppleInfoPlist(artifactFilesystem, bundlePath, cfg, options, buildNumber)
- if !plist.OK {
- return plist
- }
-
- entitlementsPath := resolveAppleEntitlementsPath(cfg, outputDir, name, options)
- entitlements := WriteAppleEntitlements(artifactFilesystem, entitlementsPath, DefaultAppleEntitlements())
- if !entitlements.OK {
- return entitlements
- }
-
- if options.Sign {
- signed := b.signAppleArtifact(ctx, cfg, bundlePath, entitlementsPath, options)
- if !signed.OK {
- return signed
- }
- }
-
- distributionPath := bundlePath
- if options.DMG.Enabled {
- dmgPath := options.DMG.OutputPath
- if dmgPath == "" {
- dmgPath = ax.Join(outputDir, name+".dmg")
- }
- dmgConfig := options.DMG
- dmgConfig.OutputPath = dmgPath
- if dmgConfig.VolumeName == "" {
- dmgConfig.VolumeName = name
- }
- createdDMG := b.CreateDMG(ctx, artifactFilesystem, bundlePath, dmgConfig)
- if !createdDMG.OK {
- return createdDMG
- }
- distributionPath = dmgPath
- }
-
- if options.notariseEnabled() {
- notarised := b.Notarise(ctx, distributionPath, options)
- if !notarised.OK {
- return notarised
- }
- }
-
- if options.TestFlight {
- uploaded := b.uploadTestFlight(ctx, cfg, bundlePath, options)
- if !uploaded.OK {
- return uploaded
- }
- }
-
- return core.Ok([]build.Artifact{{
- Path: distributionPath,
- OS: "darwin",
- Arch: targetArch,
- }})
-}
-
-func (b *AppleBuilder) buildBundle(ctx context.Context, sourceFS, artifactFS coreio.Medium, cfg *build.Config, outputDir, name, arch string) core.Result {
- switch arch {
- case "universal":
- arm64 := b.BuildWailsMacOS(ctx, artifactFS, cfg, ax.Join(outputDir, "arm64"), name, "arm64")
- if !arm64.OK {
- return arm64
- }
- arm64Path := arm64.Value.(string)
- amd64 := b.BuildWailsMacOS(ctx, artifactFS, cfg, ax.Join(outputDir, "amd64"), name, "amd64")
- if !amd64.OK {
- return amd64
- }
- amd64Path := amd64.Value.(string)
- outputPath := ax.Join(outputDir, name+".app")
- universal := b.CreateUniversal(ctx, sourceFS, artifactFS, arm64Path, amd64Path, outputPath, name)
- if !universal.OK {
- return universal
- }
- return core.Ok(outputPath)
- case "arm64", "amd64":
- return b.BuildWailsMacOS(ctx, artifactFS, cfg, outputDir, name, arch)
- default:
- return core.Fail(core.E("AppleBuilder.Build", "unsupported Apple arch: "+arch, nil))
- }
-}
-
-// BuildWailsMacOS records the Wails macOS build invocation and creates a placeholder .app bundle.
-func (b *AppleBuilder) BuildWailsMacOS(ctx context.Context, filesystem coreio.Medium, cfg *build.Config, outputDir, name, arch string) core.Result {
- if filesystem == nil {
- filesystem = coreio.Local
- }
- created := filesystem.EnsureDir(outputDir)
- if !created.OK {
- return core.Fail(core.E("AppleBuilder.BuildWailsMacOS", "failed to create Wails output directory", core.NewError(created.Error())))
- }
-
- args := []string{"build", "-platform", "darwin/" + arch}
- if len(cfg.BuildTags) > 0 {
- args = append(args, "-tags", core.Join(",", cfg.BuildTags...))
- }
- if len(cfg.LDFlags) > 0 {
- args = append(args, "-ldflags", core.Join(" ", cfg.LDFlags...))
- }
-
- // TODO(#484): this requires macOS with Wails and Xcode tooling. The skeleton
- // records the command invocation instead of executing it in sandbox.
- ran := b.runExternal(ctx, "wails-build", RunOptions{
- Command: "wails3",
- Args: args,
- Dir: cfg.ProjectDir,
- Env: build.BuildEnvironment(cfg, "GOOS=darwin", "GOARCH="+arch, "CGO_ENABLED=1"),
- })
- if !ran.OK {
- return ran
- }
-
- bundlePath := ax.Join(outputDir, name+".app")
- createdBundle := createAppleBundleSkeleton(filesystem, bundlePath, name, arch)
- if !createdBundle.OK {
- return createdBundle
- }
- return core.Ok(bundlePath)
-}
-
-// CreateUniversal records the lipo invocation and creates a placeholder universal .app bundle.
-func (b *AppleBuilder) CreateUniversal(ctx context.Context, _ coreio.Medium, artifactFS coreio.Medium, arm64Path, amd64Path, outputPath, name string) core.Result {
- if artifactFS == nil {
- artifactFS = coreio.Local
- }
- if artifactFS.Exists(outputPath) {
- deleted := artifactFS.DeleteAll(outputPath)
- if !deleted.OK {
- return core.Fail(core.E("AppleBuilder.CreateUniversal", "failed to replace universal app bundle", core.NewError(deleted.Error())))
- }
- }
- copied := build.CopyMediumPath(artifactFS, arm64Path, artifactFS, outputPath)
- if !copied.OK {
- return core.Fail(core.E("AppleBuilder.CreateUniversal", "failed to copy arm64 app bundle", core.NewError(copied.Error())))
- }
-
- armBinary := ax.Join(arm64Path, "Contents", "MacOS", name)
- amdBinary := ax.Join(amd64Path, "Contents", "MacOS", name)
- outBinary := ax.Join(outputPath, "Contents", "MacOS", name)
-
- // TODO(#484): this requires macOS lipo. The skeleton records the command
- // invocation so operators can wire execution on a real macOS runner.
- return b.runExternal(ctx, "lipo-universal", RunOptions{
- Command: "lipo",
- Args: []string{"-create", "-output", outBinary, armBinary, amdBinary},
- })
-}
-
-func (b *AppleBuilder) signAppleArtifact(ctx context.Context, cfg *build.Config, appPath, entitlementsPath string, options AppleOptions) core.Result {
- args := []string{
- "--sign", options.signingIdentity(),
- "--timestamp",
- "--force",
- "--options", "runtime",
- "--entitlements", entitlementsPath,
- appPath,
- }
-
- // TODO(#484): this requires macOS codesign identities and keychain access.
- return b.runExternal(ctx, "codesign", RunOptions{
- Command: "codesign",
- Args: args,
- Dir: cfg.ProjectDir,
- })
-}
-
-func (b *AppleBuilder) uploadTestFlight(ctx context.Context, cfg *build.Config, appPath string, options AppleOptions) core.Result {
- keyID := firstNonEmptyApple(options.TestFlightKeyID, options.APIKeyID)
- issuerID := firstNonEmptyApple(options.TestFlightIssuerID, options.APIKeyIssuerID)
- keyPath := firstNonEmptyApple(options.TestFlightKeyPath, options.APIKeyPath, options.TestFlightPrivateKey)
-
- // TODO(#484): this requires Apple Developer App Store Connect API credentials.
- return b.runExternal(ctx, "testflight-upload", RunOptions{
- Command: "xcrun",
- Args: []string{
- "altool", "--upload-app",
- "--type", "macos",
- "--file", appPath,
- "--apiKey", keyID,
- "--apiIssuer", issuerID,
- "--private-key", keyPath,
- },
- Dir: cfg.ProjectDir,
- })
-}
-
-func (b *AppleBuilder) runExternal(ctx context.Context, step string, opts RunOptions) core.Result {
- b.printTODO(step, opts)
- if firstNonEmptyApple(b.hostOS, runtime.GOOS) != "darwin" {
- return core.Ok(nil)
- }
- if b.runner == nil {
- return core.Ok(nil)
- }
- ran := b.runner.Run(ctx, opts)
- if !ran.OK {
- return core.Fail(core.E("AppleBuilder.runExternal", "stubbed "+step+" invocation failed", core.NewError(ran.Error())))
- }
- return core.Ok(nil)
-}
-
-func (b *AppleBuilder) printTODO(step string, opts RunOptions) {
- writer := b.todoWriter
- if writer == nil {
- return
- }
-
- message := appleTODOMessage{
- Level: "todo",
- Component: "apple-build",
- Step: step,
- Command: opts.Command,
- Args: append([]string{}, opts.Args...),
- Dir: opts.Dir,
- HostOS: firstNonEmptyApple(b.hostOS, runtime.GOOS),
- Requirement: "this requires macOS with Apple Developer tooling and credentials",
- }
- if message.HostOS != "darwin" {
- message.Requirement = "this requires macOS; sandbox stub did not execute external CLI"
- }
-
- encoded := core.JSONMarshal(message)
- if !encoded.OK {
- if written := core.WriteString(writer, core.Sprintf(`{"level":"todo","component":"apple-build","step":%q}`+"\n", step)); !written.OK {
- return
- }
- return
- }
- if written := core.WriteString(writer, string(encoded.Value.([]byte))+"\n"); !written.OK {
- return
- }
-}
-
-func (b *AppleBuilder) options() AppleOptions {
- if b == nil {
- return DefaultAppleBuilderOptions()
- }
- return b.Options.withDefaults()
-}
-
-type appleTODOMessage struct {
- Level string `json:"level"`
- Component string `json:"component"`
- Step string `json:"step"`
- Command string `json:"command"`
- Args []string `json:"args"`
- Dir string `json:"dir,omitempty"`
- HostOS string `json:"host_os"`
- Requirement string `json:"requirement"`
-}
-
-func (options AppleOptions) withDefaults() AppleOptions {
- defaults := DefaultAppleBuilderOptions()
- if options.Arch == "" {
- options.Arch = defaults.Arch
- }
- if options.MinSystemVersion == "" {
- options.MinSystemVersion = defaults.MinSystemVersion
- }
- if options.Category == "" {
- options.Category = defaults.Category
- }
- if options.DMG.IconSize <= 0 {
- options.DMG.IconSize = defaults.DMG.IconSize
- }
- if options.DMG.WindowSize[0] <= 0 || options.DMG.WindowSize[1] <= 0 {
- options.DMG.WindowSize = defaults.DMG.WindowSize
- }
- return options
-}
-
-func (options AppleOptions) signingIdentity() string {
- return firstNonEmptyApple(options.SigningIdentity, options.CertIdentity)
-}
-
-func (options AppleOptions) notariseEnabled() bool {
- return options.Notarise || options.Notarize
-}
-
-func (options AppleOptions) notarisationProfile() string {
- return firstNonEmptyApple(options.NotarisationProfile, options.NotarizationProfile, options.NotaryProfile)
-}
-
-// ValidateAppleOptions checks the minimum Apple pipeline option contract.
-func ValidateAppleOptions(options AppleOptions) core.Result {
- options = options.withDefaults()
-
- if core.Trim(options.BundleID) == "" {
- return core.Fail(core.E("AppleBuilder.ValidateOptions", "bundle ID is required", nil))
- }
-
- switch options.Arch {
- case "universal", "arm64", "amd64":
- default:
- return core.Fail(core.E("AppleBuilder.ValidateOptions", "arch must be universal, arm64, or amd64", nil))
- }
-
- if options.Sign && core.Trim(options.signingIdentity()) == "" {
- return core.Fail(core.E("AppleBuilder.ValidateOptions", "signing identity is required when signing is enabled", nil))
- }
-
- if options.notariseEnabled() {
- hasProfile := core.Trim(options.notarisationProfile()) != ""
- hasAPIKey := core.Trim(options.APIKeyID) != "" && core.Trim(options.APIKeyIssuerID) != "" && core.Trim(options.APIKeyPath) != ""
- hasAppleID := core.Trim(options.TeamID) != "" &&
- core.Trim(options.AppleID) != "" &&
- core.Trim(firstNonEmptyApple(options.AppPassword, options.Password)) != ""
- if !hasProfile && !hasAPIKey && !hasAppleID {
- return core.Fail(core.E("AppleBuilder.ValidateOptions", "notarisation requires a notarytool profile, API key, or Apple ID credentials", nil))
- }
- }
-
- if options.TestFlight {
- keyID := firstNonEmptyApple(options.TestFlightKeyID, options.APIKeyID)
- issuerID := firstNonEmptyApple(options.TestFlightIssuerID, options.APIKeyIssuerID)
- keyPath := firstNonEmptyApple(options.TestFlightKeyPath, options.APIKeyPath, options.TestFlightPrivateKey)
- if keyID == "" || issuerID == "" || keyPath == "" {
- return core.Fail(core.E("AppleBuilder.ValidateOptions", "TestFlight upload requires key id, issuer id, and key path", nil))
- }
- }
-
- return core.Ok(nil)
-}
-
-func resolveAppleBuilderOutputDir(cfg *build.Config, artifactFilesystem coreio.Medium) string {
- if cfg.OutputDir != "" {
- return cfg.OutputDir
- }
- if build.MediumIsLocal(artifactFilesystem) {
- return ax.Join(cfg.ProjectDir, "dist", "apple")
- }
- return "dist/apple"
-}
-
-func resolveAppleBuilderName(cfg *build.Config) string {
- if cfg.Name != "" {
- return cfg.Name
- }
- if cfg.Project.Binary != "" {
- return cfg.Project.Binary
- }
- if cfg.Project.Name != "" {
- return cfg.Project.Name
- }
- if cfg.ProjectDir != "" {
- return ax.Base(cfg.ProjectDir)
- }
- return "App"
-}
-
-func resolveAppleBuilderArch(options AppleOptions, targets []build.Target) string {
- if options.Arch != "" {
- return options.Arch
- }
- for _, target := range targets {
- if target.OS == "darwin" && target.Arch != "" {
- return target.Arch
- }
- }
- return defaultAppleBuilderArch
-}
-
-func resolveAppleEntitlementsPath(cfg *build.Config, outputDir, name string, options AppleOptions) string {
- if options.EntitlementsPath == "" {
- return ax.Join(outputDir, name+".entitlements.plist")
- }
- if ax.IsAbs(options.EntitlementsPath) || cfg == nil || cfg.ProjectDir == "" {
- return options.EntitlementsPath
- }
- return ax.Join(cfg.ProjectDir, options.EntitlementsPath)
-}
-
-func createAppleBundleSkeleton(filesystem coreio.Medium, bundlePath, name, arch string) core.Result {
- if filesystem == nil {
- filesystem = coreio.Local
- }
-
- macosDir := ax.Join(bundlePath, "Contents", "MacOS")
- resourcesDir := ax.Join(bundlePath, "Contents", "Resources")
- if created := filesystem.EnsureDir(macosDir); !created.OK {
- return core.Fail(core.E("AppleBuilder.createBundleSkeleton", "failed to create Contents/MacOS", core.NewError(created.Error())))
- }
- if created := filesystem.EnsureDir(resourcesDir); !created.OK {
- return core.Fail(core.E("AppleBuilder.createBundleSkeleton", "failed to create Contents/Resources", core.NewError(created.Error())))
- }
-
- executable := ax.Join(macosDir, name)
- content := "#!/usr/bin/env sh\n" +
- "echo \"AppleBuilder skeleton placeholder for " + name + " (" + arch + ")\"\n"
- written := filesystem.WriteMode(executable, content, 0o755)
- if !written.OK {
- return core.Fail(core.E("AppleBuilder.createBundleSkeleton", "failed to write placeholder executable", core.NewError(written.Error())))
- }
- return core.Ok(nil)
-}
-
-func firstNonEmptyApple(values ...string) string {
- for _, value := range values {
- if core.Trim(value) != "" {
- return value
- }
- }
- return ""
-}
-
-var _ build.Builder = (*AppleBuilder)(nil)
diff --git a/pkg/build/builders/apple_dmg.go b/pkg/build/builders/apple_dmg.go
deleted file mode 100644
index 419f873..0000000
--- a/pkg/build/builders/apple_dmg.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package builders
-
-import (
- "context"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-// CreateDMG records the hdiutil DMG creation flow and writes a placeholder DMG.
-func (b *AppleBuilder) CreateDMG(ctx context.Context, filesystem coreio.Medium, appPath string, cfg AppleDMGConfig) core.Result {
- if filesystem == nil {
- filesystem = coreio.Local
- }
- if appPath == "" {
- return core.Fail(core.E("AppleBuilder.CreateDMG", "app path is required", nil))
- }
- if cfg.OutputPath == "" {
- return core.Fail(core.E("AppleBuilder.CreateDMG", "output path is required", nil))
- }
- if cfg.VolumeName == "" {
- cfg.VolumeName = core.TrimSuffix(ax.Base(appPath), ".app")
- }
- if cfg.IconSize <= 0 {
- cfg.IconSize = 128
- }
- if cfg.WindowSize[0] <= 0 || cfg.WindowSize[1] <= 0 {
- cfg.WindowSize = [2]int{640, 480}
- }
-
- outputDir := ax.Dir(cfg.OutputPath)
- if outputDir != "" && outputDir != "." {
- created := filesystem.EnsureDir(outputDir)
- if !created.OK {
- return core.Fail(core.E("AppleBuilder.CreateDMG", "failed to create DMG output directory", core.NewError(created.Error())))
- }
- }
-
- stageDMG := cfg.OutputPath + ".rw"
- mountPoint := cfg.OutputPath + ".mount"
-
- // TODO(#484): hdiutil requires macOS. The skeleton records each
- // command invocation and writes a placeholder DMG for downstream lanes.
- created := b.runExternal(ctx, "hdiutil-create", RunOptions{
- Command: "hdiutil",
- Args: []string{
- "create",
- "-volname", cfg.VolumeName,
- "-srcfolder", appPath,
- "-ov",
- "-format", "UDRW",
- stageDMG,
- },
- })
- if !created.OK {
- return created
- }
-
- attached := b.runExternal(ctx, "hdiutil-attach", RunOptions{
- Command: "hdiutil",
- Args: []string{
- "attach",
- "-readwrite",
- "-noverify",
- "-noautoopen",
- "-mountpoint", mountPoint,
- stageDMG,
- },
- })
- if !attached.OK {
- return attached
- }
-
- detached := b.runExternal(ctx, "hdiutil-detach", RunOptions{
- Command: "hdiutil",
- Args: []string{"detach", mountPoint},
- })
- if !detached.OK {
- return detached
- }
-
- converted := b.runExternal(ctx, "hdiutil-convert", RunOptions{
- Command: "hdiutil",
- Args: []string{
- "convert",
- stageDMG,
- "-format", "UDZO",
- "-ov",
- "-o", cfg.OutputPath,
- },
- })
- if !converted.OK {
- return converted
- }
-
- placeholder := core.Sprintf(
- "AppleBuilder DMG skeleton\napp=%s\nvolume=%s\nbackground=%s\n",
- appPath,
- cfg.VolumeName,
- cfg.BackgroundPath,
- )
- written := filesystem.WriteMode(cfg.OutputPath, placeholder, 0o644)
- if !written.OK {
- return core.Fail(core.E("AppleBuilder.CreateDMG", "failed to write placeholder DMG", core.NewError(written.Error())))
- }
-
- return core.Ok(nil)
-}
diff --git a/pkg/build/builders/apple_dmg_example_test.go b/pkg/build/builders/apple_dmg_example_test.go
deleted file mode 100644
index 1868ef7..0000000
--- a/pkg/build/builders/apple_dmg_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleAppleBuilder_CreateDMG references AppleBuilder.CreateDMG on this package API surface.
-func ExampleAppleBuilder_CreateDMG() {
- _ = (*AppleBuilder).CreateDMG
- core.Println("AppleBuilder.CreateDMG")
- // Output: AppleBuilder.CreateDMG
-}
diff --git a/pkg/build/builders/apple_dmg_test.go b/pkg/build/builders/apple_dmg_test.go
deleted file mode 100644
index b76ef80..0000000
--- a/pkg/build/builders/apple_dmg_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package builders
-
-import (
- "context"
-
- core "dappco.re/go"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-func TestAppleDmg_AppleBuilder_CreateDMG_Good(t *core.T) {
- fs := coreio.NewMemoryMedium()
- runner := &recordingAppleRunner{}
- builder := NewAppleBuilder(WithAppleCommandRunner(runner))
-
- result := builder.CreateDMG(context.Background(), fs, "dist/Core.app", AppleDMGConfig{OutputPath: "dist/Core.dmg", VolumeName: "Core"})
- core.RequireTrue(t, result.OK)
- core.AssertLen(t, runner.calls, 4)
- core.AssertTrue(t, fs.IsFile("dist/Core.dmg"))
-}
-
-func TestAppleDmg_AppleBuilder_CreateDMG_Bad(t *core.T) {
- builder := NewAppleBuilder(WithAppleCommandRunner(&recordingAppleRunner{}))
- result := builder.CreateDMG(context.Background(), coreio.NewMemoryMedium(), "", AppleDMGConfig{OutputPath: "dist/Core.dmg"})
- core.AssertFalse(t, result.OK)
-}
-
-func TestAppleDmg_AppleBuilder_CreateDMG_Ugly(t *core.T) {
- fs := coreio.NewMemoryMedium()
- builder := NewAppleBuilder(WithAppleCommandRunner(&recordingAppleRunner{}))
-
- result := builder.CreateDMG(context.Background(), fs, "dist/Edge.app", AppleDMGConfig{OutputPath: "Core.dmg"})
- core.RequireTrue(t, result.OK)
- core.AssertTrue(t, fs.IsFile("Core.dmg"))
-}
diff --git a/pkg/build/builders/apple_example_test.go b/pkg/build/builders/apple_example_test.go
deleted file mode 100644
index 9eb2b1b..0000000
--- a/pkg/build/builders/apple_example_test.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleAppleCommandRunnerFunc_Run references AppleCommandRunnerFunc.Run on this package API surface.
-func ExampleAppleCommandRunnerFunc_Run() {
- _ = (*AppleCommandRunnerFunc).Run
- core.Println("AppleCommandRunnerFunc.Run")
- // Output: AppleCommandRunnerFunc.Run
-}
-
-// ExampleGoProcessAppleRunner_Run references GoProcessAppleRunner.Run on this package API surface.
-func ExampleGoProcessAppleRunner_Run() {
- _ = (*GoProcessAppleRunner).Run
- core.Println("GoProcessAppleRunner.Run")
- // Output: GoProcessAppleRunner.Run
-}
-
-// ExampleNewAppleBuilder references NewAppleBuilder on this package API surface.
-func ExampleNewAppleBuilder() {
- _ = NewAppleBuilder
- core.Println("NewAppleBuilder")
- // Output: NewAppleBuilder
-}
-
-// ExampleWithAppleOptions references WithAppleOptions on this package API surface.
-func ExampleWithAppleOptions() {
- _ = WithAppleOptions
- core.Println("WithAppleOptions")
- // Output: WithAppleOptions
-}
-
-// ExampleWithAppleCommandRunner references WithAppleCommandRunner on this package API surface.
-func ExampleWithAppleCommandRunner() {
- _ = WithAppleCommandRunner
- core.Println("WithAppleCommandRunner")
- // Output: WithAppleCommandRunner
-}
-
-// ExampleWithAppleHostOS references WithAppleHostOS on this package API surface.
-func ExampleWithAppleHostOS() {
- _ = WithAppleHostOS
- core.Println("WithAppleHostOS")
- // Output: WithAppleHostOS
-}
-
-// ExampleWithAppleTODOWriter references WithAppleTODOWriter on this package API surface.
-func ExampleWithAppleTODOWriter() {
- _ = WithAppleTODOWriter
- core.Println("WithAppleTODOWriter")
- // Output: WithAppleTODOWriter
-}
-
-// ExampleDefaultAppleBuilderOptions references DefaultAppleBuilderOptions on this package API surface.
-func ExampleDefaultAppleBuilderOptions() {
- _ = DefaultAppleBuilderOptions
- core.Println("DefaultAppleBuilderOptions")
- // Output: DefaultAppleBuilderOptions
-}
-
-// ExampleAppleBuilder_Name references AppleBuilder.Name on this package API surface.
-func ExampleAppleBuilder_Name() {
- _ = (*AppleBuilder).Name
- core.Println("AppleBuilder.Name")
- // Output: AppleBuilder.Name
-}
-
-// ExampleAppleBuilder_Detect references AppleBuilder.Detect on this package API surface.
-func ExampleAppleBuilder_Detect() {
- _ = (*AppleBuilder).Detect
- core.Println("AppleBuilder.Detect")
- // Output: AppleBuilder.Detect
-}
-
-// ExampleAppleBuilder_Build references AppleBuilder.Build on this package API surface.
-func ExampleAppleBuilder_Build() {
- _ = (*AppleBuilder).Build
- core.Println("AppleBuilder.Build")
- // Output: AppleBuilder.Build
-}
-
-// ExampleAppleBuilder_BuildWailsMacOS references AppleBuilder.BuildWailsMacOS on this package API surface.
-func ExampleAppleBuilder_BuildWailsMacOS() {
- _ = (*AppleBuilder).BuildWailsMacOS
- core.Println("AppleBuilder.BuildWailsMacOS")
- // Output: AppleBuilder.BuildWailsMacOS
-}
-
-// ExampleAppleBuilder_CreateUniversal references AppleBuilder.CreateUniversal on this package API surface.
-func ExampleAppleBuilder_CreateUniversal() {
- _ = (*AppleBuilder).CreateUniversal
- core.Println("AppleBuilder.CreateUniversal")
- // Output: AppleBuilder.CreateUniversal
-}
-
-// ExampleValidateAppleOptions references ValidateAppleOptions on this package API surface.
-func ExampleValidateAppleOptions() {
- _ = ValidateAppleOptions
- core.Println("ValidateAppleOptions")
- // Output: ValidateAppleOptions
-}
diff --git a/pkg/build/builders/apple_notarise.go b/pkg/build/builders/apple_notarise.go
deleted file mode 100644
index ca9737f..0000000
--- a/pkg/build/builders/apple_notarise.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package builders
-
-import (
- "context"
-
- "dappco.re/go"
-)
-
-// AppleNotariseConfig defines a notarisation request for a built Apple artifact.
-type AppleNotariseConfig struct {
- AppPath string
- Profile string
- APIKeyID string
- APIKeyIssuerID string
- APIKeyPath string
- TeamID string
- AppleID string
- Password string
-}
-
-// Notarise records notarytool submit and stapler staple invocations.
-// A real run requires Apple Developer credentials, either through a
-// notarytool keychain profile, App Store Connect API key, or Apple ID credentials.
-func (b *AppleBuilder) Notarise(ctx context.Context, artifactPath string, options AppleOptions) core.Result {
- if artifactPath == "" {
- return core.Fail(core.E("AppleBuilder.Notarise", "artifact path is required", nil))
- }
-
- submitArgs := []string{
- "notarytool",
- "submit",
- artifactPath,
- "--wait",
- }
- submitArgs = append(submitArgs, appleNotaryAuthArgs(options)...)
-
- // TODO(#484): xcrun notarytool requires macOS and Apple Developer
- // credentials. The skeleton records the command invocation only.
- submitted := b.runExternal(ctx, "notarytool-submit", RunOptions{
- Command: "xcrun",
- Args: submitArgs,
- })
- if !submitted.OK {
- return submitted
- }
-
- // TODO(#484): xcrun stapler requires a notarised artifact on macOS.
- return b.runExternal(ctx, "stapler-staple", RunOptions{
- Command: "xcrun",
- Args: []string{"stapler", "staple", artifactPath},
- })
-}
-
-func appleNotaryAuthArgs(options AppleOptions) []string {
- if profile := options.notarisationProfile(); profile != "" {
- return []string{"--keychain-profile", profile}
- }
-
- if options.APIKeyID != "" {
- return []string{
- "--key", options.APIKeyPath,
- "--key-id", options.APIKeyID,
- "--issuer", options.APIKeyIssuerID,
- }
- }
-
- return []string{
- "--apple-id", options.AppleID,
- "--password", firstNonEmptyApple(options.AppPassword, options.Password),
- "--team-id", options.TeamID,
- }
-}
diff --git a/pkg/build/builders/apple_notarise_example_test.go b/pkg/build/builders/apple_notarise_example_test.go
deleted file mode 100644
index 7a5e582..0000000
--- a/pkg/build/builders/apple_notarise_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleAppleBuilder_Notarise references AppleBuilder.Notarise on this package API surface.
-func ExampleAppleBuilder_Notarise() {
- _ = (*AppleBuilder).Notarise
- core.Println("AppleBuilder.Notarise")
- // Output: AppleBuilder.Notarise
-}
diff --git a/pkg/build/builders/apple_notarise_test.go b/pkg/build/builders/apple_notarise_test.go
deleted file mode 100644
index 8953bdc..0000000
--- a/pkg/build/builders/apple_notarise_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package builders
-
-import (
- "context"
-
- core "dappco.re/go"
-)
-
-func TestAppleNotarise_AppleBuilder_Notarise_Good(t *core.T) {
- runner := &recordingAppleRunner{}
- builder := NewAppleBuilder(WithAppleCommandRunner(runner))
-
- result := builder.Notarise(context.Background(), "dist/Core.zip", AppleOptions{NotarisationProfile: "core-notary"})
- core.RequireTrue(t, result.OK)
- core.AssertLen(t, runner.calls, 2)
- core.AssertContains(t, runner.calls[0].Args, "--keychain-profile")
-}
-
-func TestAppleNotarise_AppleBuilder_Notarise_Bad(t *core.T) {
- builder := NewAppleBuilder(WithAppleCommandRunner(&recordingAppleRunner{}))
- result := builder.Notarise(context.Background(), "", AppleOptions{})
- core.AssertFalse(t, result.OK)
-}
-
-func TestAppleNotarise_AppleBuilder_Notarise_Ugly(t *core.T) {
- runner := &recordingAppleRunner{}
- builder := NewAppleBuilder(WithAppleCommandRunner(runner))
-
- result := builder.Notarise(context.Background(), "dist/Core.zip", AppleOptions{APIKeyID: "KEY", APIKeyIssuerID: "ISSUER", APIKeyPath: "AuthKey.p8"})
- core.RequireTrue(t, result.OK)
- core.AssertContains(t, runner.calls[0].Args, "--issuer")
-}
diff --git a/pkg/build/builders/apple_plist.go b/pkg/build/builders/apple_plist.go
deleted file mode 100644
index 8f27dc7..0000000
--- a/pkg/build/builders/apple_plist.go
+++ /dev/null
@@ -1,286 +0,0 @@
-package builders
-
-import (
- "sort"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-// AppleInfoPlist contains the generated macOS app bundle metadata.
-type AppleInfoPlist struct {
- BundleID string
- BundleName string
- BundleDisplayName string
- BundleVersion string
- BuildNumber string
- Executable string
- MinSystemVersion string
- Category string
- Copyright string
-}
-
-// AppleEntitlements contains the default macOS sandbox entitlements.
-type AppleEntitlements struct {
- HardenedRuntime bool
- AppSandbox bool
- NetworkClient bool
-}
-
-// GenerateAppleInfoPlist creates Info.plist metadata from the build Config.
-func GenerateAppleInfoPlist(cfg *build.Config, options AppleOptions, buildNumber string) AppleInfoPlist {
- name := "App"
- version := "0.0.0"
- if cfg != nil {
- name = resolveAppleBuilderName(cfg)
- version = normalizeAppleBuilderVersion(cfg.Version)
- }
- if buildNumber == "" {
- buildNumber = "1"
- }
-
- options = options.withDefaults()
- return AppleInfoPlist{
- BundleID: options.BundleID,
- BundleName: name,
- BundleDisplayName: firstNonEmptyApple(options.BundleDisplayName, name),
- BundleVersion: version,
- BuildNumber: buildNumber,
- Executable: name,
- MinSystemVersion: options.MinSystemVersion,
- Category: options.Category,
- Copyright: options.Copyright,
- }
-}
-
-// WriteAppleInfoPlist writes Contents/Info.plist for appPath.
-func WriteAppleInfoPlist(filesystem coreio.Medium, appPath string, cfg *build.Config, options AppleOptions, buildNumber string) core.Result {
- if filesystem == nil {
- filesystem = coreio.Local
- }
- if appPath == "" {
- return core.Fail(core.E("AppleBuilder.WriteInfoPlist", "app path is required", nil))
- }
-
- plist := GenerateAppleInfoPlist(cfg, options, buildNumber)
- path := ax.Join(appPath, "Contents", "Info.plist")
- created := filesystem.EnsureDir(ax.Dir(path))
- if !created.OK {
- return core.Fail(core.E("AppleBuilder.WriteInfoPlist", "failed to create Info.plist directory", core.NewError(created.Error())))
- }
- written := filesystem.WriteMode(path, encodeApplePlist(plist.Values()), 0o644)
- if !written.OK {
- return core.Fail(core.E("AppleBuilder.WriteInfoPlist", "failed to write Info.plist", core.NewError(written.Error())))
- }
- return core.Ok(path)
-}
-
-// Values converts the plist metadata to Apple Info.plist keys.
-func (plist AppleInfoPlist) Values() map[string]any {
- return map[string]any{
- "CFBundleDevelopmentRegion": "en",
- "CFBundleDisplayName": plist.BundleDisplayName,
- "CFBundleExecutable": plist.Executable,
- "CFBundleIdentifier": plist.BundleID,
- "CFBundleInfoDictionaryVersion": "6.0",
- "CFBundleName": plist.BundleName,
- "CFBundlePackageType": "APPL",
- "CFBundleShortVersionString": plist.BundleVersion,
- "CFBundleVersion": plist.BuildNumber,
- "LSApplicationCategoryType": plist.Category,
- "LSMinimumSystemVersion": plist.MinSystemVersion,
- "NSHighResolutionCapable": true,
- "NSHumanReadableCopyright": plist.Copyright,
- "NSSupportsSecureRestorableState": true,
- }
-}
-
-// DefaultAppleEntitlements returns the skeleton hardened runtime, sandbox, and network-client entitlements.
-func DefaultAppleEntitlements() AppleEntitlements {
- return AppleEntitlements{
- HardenedRuntime: true,
- AppSandbox: true,
- NetworkClient: true,
- }
-}
-
-// WriteAppleEntitlements writes a macOS entitlements plist.
-func WriteAppleEntitlements(filesystem coreio.Medium, path string, entitlements AppleEntitlements) core.Result {
- if filesystem == nil {
- filesystem = coreio.Local
- }
- if path == "" {
- return core.Fail(core.E("AppleBuilder.WriteEntitlements", "entitlements path is required", nil))
- }
- created := filesystem.EnsureDir(ax.Dir(path))
- if !created.OK {
- return core.Fail(core.E("AppleBuilder.WriteEntitlements", "failed to create entitlements directory", core.NewError(created.Error())))
- }
- written := filesystem.WriteMode(path, encodeApplePlist(entitlements.Values()), 0o644)
- if !written.OK {
- return core.Fail(core.E("AppleBuilder.WriteEntitlements", "failed to write entitlements", core.NewError(written.Error())))
- }
- return core.Ok(nil)
-}
-
-// Values converts entitlements to Apple entitlement keys.
-func (entitlements AppleEntitlements) Values() map[string]any {
- return map[string]any{
- "com.apple.security.app-sandbox": entitlements.AppSandbox,
- "com.apple.security.cs.allow-unsigned-executable-memory": entitlements.HardenedRuntime,
- "com.apple.security.network.client": entitlements.NetworkClient,
- }
-}
-
-// WriteXcodeCloudConfig writes the AppleBuilder Xcode Cloud script templates.
-func (b *AppleBuilder) WriteXcodeCloudConfig(filesystem coreio.Medium, projectDir string, cfg *build.Config, options AppleOptions) core.Result {
- if filesystem == nil {
- filesystem = coreio.Local
- }
- baseDir := ax.Join(projectDir, ".xcode-cloud", "ci_scripts")
- created := filesystem.EnsureDir(baseDir)
- if !created.OK {
- return core.Fail(core.E("AppleBuilder.WriteXcodeCloudConfig", "failed to create Xcode Cloud scripts directory", core.NewError(created.Error())))
- }
-
- name := "App"
- if cfg != nil {
- name = resolveAppleBuilderName(cfg)
- }
- buildCommand := "core build apple --config .core/build.yaml --arch " + shellQuoteApple(options.withDefaults().Arch)
-
- scripts := map[string]string{
- "ci_post_clone.sh": xcodeCloudPostCloneScript(),
- "ci_pre_xcodebuild.sh": xcodeCloudPreXcodebuildScript(buildCommand),
- "ci_post_xcodebuild.sh": xcodeCloudPostXcodebuildScript(name),
- }
-
- ordered := []string{"ci_post_clone.sh", "ci_pre_xcodebuild.sh", "ci_post_xcodebuild.sh"}
- paths := make([]string, 0, len(ordered))
- for _, name := range ordered {
- path := ax.Join(baseDir, name)
- written := filesystem.WriteMode(path, scripts[name], 0o755)
- if !written.OK {
- return core.Fail(core.E("AppleBuilder.WriteXcodeCloudConfig", "failed to write "+name, core.NewError(written.Error())))
- }
- paths = append(paths, path)
- }
- return core.Ok(paths)
-}
-
-func encodeApplePlist(values map[string]any) string {
- keys := make([]string, 0, len(values))
- for key := range values {
- keys = append(keys, key)
- }
- sort.Strings(keys)
-
- b := core.NewBuilder()
- b.WriteString(`` + "\n")
- b.WriteString(`` + "\n")
- b.WriteString(`` + "\n")
- b.WriteString("\n")
- for _, key := range keys {
- b.WriteString("\t")
- b.WriteString(escapeAppleXML(key))
- b.WriteString("\n")
- b.WriteString(applePlistValue(values[key]))
- }
- b.WriteString("\n")
- b.WriteString("\n")
- return b.String()
-}
-
-func applePlistValue(value any) string {
- switch v := value.(type) {
- case bool:
- if v {
- return "\t\n"
- }
- return "\t\n"
- case string:
- return "\t" + escapeAppleXML(v) + "\n"
- default:
- return "\t" + escapeAppleXML(core.Sprintf("%v", value)) + "\n"
- }
-}
-
-func escapeAppleXML(value string) string {
- b := core.NewBuilder()
- for _, r := range value {
- switch r {
- case '&':
- b.WriteString("&")
- case '<':
- b.WriteString("<")
- case '>':
- b.WriteString(">")
- case '"':
- b.WriteString(""")
- case '\'':
- b.WriteString("'")
- default:
- b.WriteRune(r)
- }
- }
- return b.String()
-}
-
-func normalizeAppleBuilderVersion(version string) string {
- version = core.Trim(version)
- version = core.TrimPrefix(version, "v")
- if version == "" {
- return "0.0.0"
- }
- return version
-}
-
-func xcodeCloudPostCloneScript() string {
- return core.Trim(`#!/usr/bin/env bash
-set -euo pipefail
-
-export PATH="${HOME}/go/bin:${HOME}/.deno/bin:${HOME}/.bun/bin:${PATH}"
-
-if ! command -v go >/dev/null 2>&1; then
- echo "Go is required for AppleBuilder Xcode Cloud builds." >&2
- exit 1
-fi
-
-if ! command -v wails3 >/dev/null 2>&1 && ! command -v wails >/dev/null 2>&1; then
- echo "Wails is required for AppleBuilder Xcode Cloud builds." >&2
- exit 1
-fi
-`) + "\n"
-}
-
-func xcodeCloudPreXcodebuildScript(buildCommand string) string {
- return core.Trim(`#!/usr/bin/env bash
-set -euo pipefail
-
-export PATH="${HOME}/go/bin:${HOME}/.deno/bin:${HOME}/.bun/bin:${PATH}"
-
-`+buildCommand) + "\n"
-}
-
-func xcodeCloudPostXcodebuildScript(name string) string {
- bundlePath := ax.Join("dist", "apple", name+".app")
- executablePath := ax.Join(bundlePath, "Contents", "MacOS", name)
- return core.Trim(`#!/usr/bin/env bash
-set -euo pipefail
-
-BUNDLE_PATH=`+shellQuoteApple(bundlePath)+`
-EXECUTABLE_PATH=`+shellQuoteApple(executablePath)+`
-
-test -d "$BUNDLE_PATH"
-test -x "$EXECUTABLE_PATH"
-`) + "\n"
-}
-
-func shellQuoteApple(value string) string {
- if value == "" {
- return "''"
- }
- return "'" + core.Replace(value, "'", `'"'"'`) + "'"
-}
diff --git a/pkg/build/builders/apple_plist_example_test.go b/pkg/build/builders/apple_plist_example_test.go
deleted file mode 100644
index af6683a..0000000
--- a/pkg/build/builders/apple_plist_example_test.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleGenerateAppleInfoPlist references GenerateAppleInfoPlist on this package API surface.
-func ExampleGenerateAppleInfoPlist() {
- _ = GenerateAppleInfoPlist
- core.Println("GenerateAppleInfoPlist")
- // Output: GenerateAppleInfoPlist
-}
-
-// ExampleWriteAppleInfoPlist references WriteAppleInfoPlist on this package API surface.
-func ExampleWriteAppleInfoPlist() {
- _ = WriteAppleInfoPlist
- core.Println("WriteAppleInfoPlist")
- // Output: WriteAppleInfoPlist
-}
-
-// ExampleAppleInfoPlist_Values references AppleInfoPlist.Values on this package API surface.
-func ExampleAppleInfoPlist_Values() {
- _ = (*AppleInfoPlist).Values
- core.Println("AppleInfoPlist.Values")
- // Output: AppleInfoPlist.Values
-}
-
-// ExampleDefaultAppleEntitlements references DefaultAppleEntitlements on this package API surface.
-func ExampleDefaultAppleEntitlements() {
- _ = DefaultAppleEntitlements
- core.Println("DefaultAppleEntitlements")
- // Output: DefaultAppleEntitlements
-}
-
-// ExampleWriteAppleEntitlements references WriteAppleEntitlements on this package API surface.
-func ExampleWriteAppleEntitlements() {
- _ = WriteAppleEntitlements
- core.Println("WriteAppleEntitlements")
- // Output: WriteAppleEntitlements
-}
-
-// ExampleAppleEntitlements_Values references AppleEntitlements.Values on this package API surface.
-func ExampleAppleEntitlements_Values() {
- _ = (*AppleEntitlements).Values
- core.Println("AppleEntitlements.Values")
- // Output: AppleEntitlements.Values
-}
-
-// ExampleAppleBuilder_WriteXcodeCloudConfig references AppleBuilder.WriteXcodeCloudConfig on this package API surface.
-func ExampleAppleBuilder_WriteXcodeCloudConfig() {
- _ = (*AppleBuilder).WriteXcodeCloudConfig
- core.Println("AppleBuilder.WriteXcodeCloudConfig")
- // Output: AppleBuilder.WriteXcodeCloudConfig
-}
diff --git a/pkg/build/builders/apple_plist_test.go b/pkg/build/builders/apple_plist_test.go
deleted file mode 100644
index 1a537cf..0000000
--- a/pkg/build/builders/apple_plist_test.go
+++ /dev/null
@@ -1,151 +0,0 @@
-package builders
-
-import (
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-func TestApplePlist_GenerateAppleInfoPlist_Good(t *core.T) {
- plist := GenerateAppleInfoPlist(&build.Config{Name: "Core", Version: "v1.2.3"}, AppleOptions{BundleID: "ai.lthn.core"}, "42")
- core.AssertEqual(t, "Core", plist.BundleName)
- core.AssertEqual(t, "1.2.3", plist.BundleVersion)
-}
-
-func TestApplePlist_GenerateAppleInfoPlist_Bad(t *core.T) {
- plist := GenerateAppleInfoPlist(nil, AppleOptions{}, "")
- core.AssertEqual(t, "App", plist.BundleName)
- core.AssertEqual(t, "1", plist.BuildNumber)
-}
-
-func TestApplePlist_GenerateAppleInfoPlist_Ugly(t *core.T) {
- plist := GenerateAppleInfoPlist(&build.Config{Project: build.Project{Name: "ProjectName"}}, AppleOptions{BundleDisplayName: "Display"}, "")
- core.AssertEqual(t, "ProjectName", plist.BundleName)
- core.AssertEqual(t, "Display", plist.BundleDisplayName)
-}
-
-func TestApplePlist_WriteAppleInfoPlist_Good(t *core.T) {
- fs := coreio.NewMemoryMedium()
- result := WriteAppleInfoPlist(fs, "Core.app", &build.Config{Name: "Core"}, AppleOptions{BundleID: "ai.lthn.core"}, "7")
- core.RequireTrue(t, result.OK)
- path := result.Value.(string)
- core.AssertEqual(t, "Core.app/Contents/Info.plist", path)
- core.AssertTrue(t, fs.IsFile(path))
-}
-
-func TestApplePlist_WriteAppleInfoPlist_Bad(t *core.T) {
- result := WriteAppleInfoPlist(coreio.NewMemoryMedium(), "", nil, AppleOptions{}, "")
- core.AssertFalse(t, result.OK)
- core.AssertContains(t, result.Error(), "app path is required")
-}
-
-func TestApplePlist_WriteAppleInfoPlist_Ugly(t *core.T) {
- fs := coreio.NewMemoryMedium()
- result := WriteAppleInfoPlist(fs, "Edge.app", nil, AppleOptions{}, "")
- core.RequireTrue(t, result.OK)
- path := result.Value.(string)
- readResult := fs.Read(path)
- core.RequireTrue(t, readResult.OK)
- content := readResult.Value.(string)
- core.AssertContains(t, content, "CFBundleName")
-}
-
-func TestApplePlist_AppleInfoPlist_Values_Good(t *core.T) {
- values := (AppleInfoPlist{BundleID: "ai.lthn.core", BundleName: "Core", Executable: "Core"}).Values()
- core.AssertEqual(t, "ai.lthn.core", values["CFBundleIdentifier"])
- core.AssertEqual(t, "Core", values["CFBundleExecutable"])
-}
-
-func TestApplePlist_AppleInfoPlist_Values_Bad(t *core.T) {
- values := (AppleInfoPlist{}).Values()
- core.AssertEqual(t, "", values["CFBundleIdentifier"])
- core.AssertEqual(t, true, values["NSHighResolutionCapable"])
-}
-
-func TestApplePlist_AppleInfoPlist_Values_Ugly(t *core.T) {
- values := (AppleInfoPlist{BundleVersion: "0.0.0", BuildNumber: "1"}).Values()
- core.AssertEqual(t, "0.0.0", values["CFBundleShortVersionString"])
- core.AssertEqual(t, "1", values["CFBundleVersion"])
-}
-
-func TestApplePlist_DefaultAppleEntitlements_Good(t *core.T) {
- entitlements := DefaultAppleEntitlements()
- core.AssertTrue(t, entitlements.HardenedRuntime)
- core.AssertTrue(t, entitlements.NetworkClient)
-}
-
-func TestApplePlist_DefaultAppleEntitlements_Bad(t *core.T) {
- entitlements := DefaultAppleEntitlements()
- entitlements.AppSandbox = false
- core.AssertFalse(t, entitlements.AppSandbox)
-}
-
-func TestApplePlist_DefaultAppleEntitlements_Ugly(t *core.T) {
- values := DefaultAppleEntitlements().Values()
- core.AssertEqual(t, true, values["com.apple.security.cs.allow-unsigned-executable-memory"])
- core.AssertEqual(t, true, values["com.apple.security.network.client"])
-}
-
-func TestApplePlist_WriteAppleEntitlements_Good(t *core.T) {
- fs := coreio.NewMemoryMedium()
- result := WriteAppleEntitlements(fs, "Core.app/Contents/Core.entitlements", DefaultAppleEntitlements())
- core.RequireTrue(t, result.OK)
- core.AssertTrue(t, fs.IsFile("Core.app/Contents/Core.entitlements"))
-}
-
-func TestApplePlist_WriteAppleEntitlements_Bad(t *core.T) {
- result := WriteAppleEntitlements(coreio.NewMemoryMedium(), "", DefaultAppleEntitlements())
- core.AssertFalse(t, result.OK)
- core.AssertContains(t, result.Error(), "path is required")
-}
-
-func TestApplePlist_WriteAppleEntitlements_Ugly(t *core.T) {
- fs := coreio.NewMemoryMedium()
- result := WriteAppleEntitlements(fs, "Core.entitlements", AppleEntitlements{})
- core.RequireTrue(t, result.OK)
- core.AssertTrue(t, fs.IsFile("Core.entitlements"))
-}
-
-func TestApplePlist_AppleEntitlements_Values_Good(t *core.T) {
- values := DefaultAppleEntitlements().Values()
- core.AssertEqual(t, true, values["com.apple.security.cs.allow-unsigned-executable-memory"])
- core.AssertEqual(t, true, values["com.apple.security.app-sandbox"])
-}
-
-func TestApplePlist_AppleEntitlements_Values_Bad(t *core.T) {
- values := (AppleEntitlements{}).Values()
- core.AssertEqual(t, false, values["com.apple.security.network.client"])
- core.AssertEqual(t, false, values["com.apple.security.app-sandbox"])
-}
-
-func TestApplePlist_AppleEntitlements_Values_Ugly(t *core.T) {
- values := (AppleEntitlements{NetworkClient: true}).Values()
- core.AssertEqual(t, false, values["com.apple.security.app-sandbox"])
- core.AssertEqual(t, true, values["com.apple.security.network.client"])
-}
-
-func TestApplePlist_AppleBuilder_WriteXcodeCloudConfig_Good(t *core.T) {
- fs := coreio.NewMemoryMedium()
- result := NewAppleBuilder().WriteXcodeCloudConfig(fs, "Project", &build.Config{Name: "Core"}, AppleOptions{Arch: "universal"})
- core.RequireTrue(t, result.OK)
- paths := result.Value.([]string)
- core.AssertLen(t, paths, 3)
-}
-
-func TestApplePlist_AppleBuilder_WriteXcodeCloudConfig_Bad(t *core.T) {
- projectDir := core.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, ".xcode-cloud"), []byte("not a directory"), 0o644)
- core.RequireTrue(t, result.OK)
- result = NewAppleBuilder().WriteXcodeCloudConfig(coreio.Local, projectDir, nil, AppleOptions{})
- core.AssertFalse(t, result.OK)
- core.AssertContains(t, result.Error(), "failed to create Xcode Cloud scripts directory")
-}
-
-func TestApplePlist_AppleBuilder_WriteXcodeCloudConfig_Ugly(t *core.T) {
- fs := coreio.NewMemoryMedium()
- result := NewAppleBuilder().WriteXcodeCloudConfig(fs, ".", nil, AppleOptions{})
- core.RequireTrue(t, result.OK)
- paths := result.Value.([]string)
- core.AssertContains(t, paths, ".xcode-cloud/ci_scripts/ci_post_clone.sh")
-}
diff --git a/pkg/build/builders/apple_test.go b/pkg/build/builders/apple_test.go
deleted file mode 100644
index 3216023..0000000
--- a/pkg/build/builders/apple_test.go
+++ /dev/null
@@ -1,626 +0,0 @@
-package builders
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-var _ build.Builder = (*AppleBuilder)(nil)
-
-type recordingAppleRunner struct {
- calls []RunOptions
-}
-
-func (runner *recordingAppleRunner) Run(ctx context.Context, opts RunOptions) core.Result {
- runner.calls = append(runner.calls, opts)
- return core.Ok("ok")
-}
-
-func TestAppleBuilder_Good(t *testing.T) {
- projectDir := t.TempDir()
- outputDir := ax.Join(projectDir, "dist", "apple")
- if result := ax.WriteFile(ax.Join(projectDir, "wails.json"), []byte(`{"name":"Core"}`+"\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- todo := core.NewBuffer()
- runner := &recordingAppleRunner{}
- builder := NewAppleBuilder(
- WithAppleHostOS("darwin"),
- WithAppleCommandRunner(runner),
- WithAppleTODOWriter(todo),
- WithAppleOptions(AppleOptions{
- BundleID: "ai.lthn.core",
- SigningIdentity: "Developer ID Application: Lethean CIC (ABC123DEF4)",
- Sign: true,
- Notarise: true,
- NotarisationProfile: "core-notary",
- XcodeCloud: true,
- BuildNumber: "42",
- BundleDisplayName: "Core",
- MinSystemVersion: "13.0",
- Category: "public.app-category.developer-tools",
- DMG: AppleDMGConfig{Enabled: true, VolumeName: "Core"},
- TestFlightKeyID: "ignored",
- TestFlightIssuerID: "ignored",
- TestFlightPrivateKey: "ignored",
- }),
- )
-
- detectResult := builder.Detect(coreio.Local, projectDir)
- if !detectResult.OK {
- t.Fatalf("unexpected error: %v", detectResult.Error())
- }
- detected := detectResult.Value.(bool)
- if !(detected) {
- t.Fatal("expected true")
- }
-
- buildResult := builder.Build(context.Background(), &build.Config{
- FS: coreio.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "Core",
- Version: "v1.2.3",
- }, nil)
- if !buildResult.OK {
- t.Fatalf("unexpected error: %v", buildResult.Error())
- }
- artifacts := buildResult.Value.([]build.Artifact)
- if !stdlibAssertEqual(1, len(artifacts)) {
- t.Fatalf("want %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(ax.Join(outputDir, "Core.dmg"), artifacts[0].Path) {
- t.Fatalf("want %v, got %v", ax.Join(outputDir, "Core.dmg"), artifacts[0].Path)
- }
-
- infoPlistResult := ax.ReadFile(ax.Join(outputDir, "Core.app", "Contents", "Info.plist"))
- if !infoPlistResult.OK {
- t.Fatalf("unexpected error: %v", infoPlistResult.Error())
- }
- infoPlist := infoPlistResult.Value.([]byte)
- if !stdlibAssertContains(string(infoPlist), "CFBundleIdentifier") {
- t.Fatalf("expected Info.plist to contain bundle identifier key")
- }
- if !stdlibAssertContains(string(infoPlist), "ai.lthn.core") {
- t.Fatalf("expected Info.plist to contain bundle id")
- }
-
- entitlementsResult := ax.ReadFile(ax.Join(outputDir, "Core.entitlements.plist"))
- if !entitlementsResult.OK {
- t.Fatalf("unexpected error: %v", entitlementsResult.Error())
- }
- entitlements := entitlementsResult.Value.([]byte)
- if !stdlibAssertContains(string(entitlements), "com.apple.security.app-sandbox") {
- t.Fatalf("expected entitlements to contain app sandbox")
- }
- if !stdlibAssertContains(string(entitlements), "com.apple.security.network.client") {
- t.Fatalf("expected entitlements to contain network client")
- }
-
- for _, script := range []string{"ci_post_clone.sh", "ci_pre_xcodebuild.sh", "ci_post_xcodebuild.sh"} {
- if !coreio.Local.IsFile(ax.Join(projectDir, ".xcode-cloud", "ci_scripts", script)) {
- t.Fatalf("expected Xcode Cloud script %s", script)
- }
- }
-
- wantCommands := []string{"wails3", "wails3", "lipo", "codesign", "hdiutil", "hdiutil", "hdiutil", "hdiutil", "xcrun", "xcrun"}
- var gotCommands []string
- for _, call := range runner.calls {
- gotCommands = append(gotCommands, call.Command)
- }
- if !stdlibAssertEqual(wantCommands, gotCommands) {
- t.Fatalf("want %v, got %v", wantCommands, gotCommands)
- }
- if !stdlibAssertContains(todo.String(), `"step":"wails-build"`) {
- t.Fatalf("expected structured TODO output, got %s", todo.String())
- }
-}
-
-func TestAppleBuilder_Bad(t *testing.T) {
- result := ValidateAppleOptions(AppleOptions{})
- if result.OK {
- t.Fatal("expected missing bundle ID error")
- }
-
- result = ValidateAppleOptions(AppleOptions{
- BundleID: "ai.lthn.core",
- Sign: true,
- })
- if result.OK {
- t.Fatal("expected missing signing identity error")
- }
- if !stdlibAssertContains(result.Error(), "signing identity") {
- t.Fatalf("expected %v to contain %v", result.Error(), "signing identity")
- }
-
- result = ValidateAppleOptions(AppleOptions{
- BundleID: "ai.lthn.core",
- Notarise: true,
- })
- if result.OK {
- t.Fatal("expected missing notarisation credentials error")
- }
- if !stdlibAssertContains(result.Error(), "notarisation") {
- t.Fatalf("expected %v to contain %v", result.Error(), "notarisation")
- }
-}
-
-func TestAppleBuilder_Ugly(t *testing.T) {
- projectDir := t.TempDir()
- outputDir := ax.Join(projectDir, "dist", "apple")
- if result := ax.WriteFile(ax.Join(projectDir, "wails.json"), []byte(`{"name":"Core"}`+"\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- todo := core.NewBuffer()
- runner := &recordingAppleRunner{}
- builder := NewAppleBuilder(
- WithAppleHostOS("linux"),
- WithAppleCommandRunner(runner),
- WithAppleTODOWriter(todo),
- WithAppleOptions(AppleOptions{
- BundleID: "ai.lthn.core",
- Arch: "arm64",
- }),
- )
-
- result := builder.Build(context.Background(), &build.Config{
- FS: coreio.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "Core",
- Version: "v1.2.3",
- }, []build.Target{{OS: "darwin", Arch: "arm64"}})
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- artifacts := result.Value.([]build.Artifact)
- if !stdlibAssertEqual(ax.Join(outputDir, "Core.app"), artifacts[0].Path) {
- t.Fatalf("want %v, got %v", ax.Join(outputDir, "Core.app"), artifacts[0].Path)
- }
- if !stdlibAssertEqual(0, len(runner.calls)) {
- t.Fatalf("want no command calls outside macOS, got %v", runner.calls)
- }
- if !core.Contains(todo.String(), "this requires macOS") {
- t.Fatalf("expected non-macOS TODO, got %s", todo.String())
- }
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestApple_AppleCommandRunnerFunc_Run_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := AppleCommandRunnerFunc(func(core.Context, RunOptions) core.Result { return core.Ok("ok") })
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Run(ctx, RunOptions{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleCommandRunnerFunc_Run_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := AppleCommandRunnerFunc(func(core.Context, RunOptions) core.Result { return core.Ok("ok") })
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Run(ctx, RunOptions{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleCommandRunnerFunc_Run_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := AppleCommandRunnerFunc(func(core.Context, RunOptions) core.Result { return core.Ok("ok") })
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Run(ctx, RunOptions{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_GoProcessAppleRunner_Run_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := GoProcessAppleRunner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Run(ctx, RunOptions{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_GoProcessAppleRunner_Run_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := GoProcessAppleRunner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Run(ctx, RunOptions{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_GoProcessAppleRunner_Run_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := GoProcessAppleRunner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Run(ctx, RunOptions{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_NewAppleBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewAppleBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_NewAppleBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewAppleBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_NewAppleBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewAppleBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithAppleOptions_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleOptions(AppleOptions{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithAppleOptions_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleOptions(AppleOptions{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithAppleOptions_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleOptions(AppleOptions{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithAppleCommandRunner_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleCommandRunner(nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithAppleCommandRunner_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleCommandRunner(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithAppleCommandRunner_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleCommandRunner(nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithAppleHostOS_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleHostOS("linux")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithAppleHostOS_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleHostOS("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithAppleHostOS_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleHostOS("linux")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_WithAppleTODOWriter_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleTODOWriter(core.NewBuffer())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_WithAppleTODOWriter_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleTODOWriter(core.NewBuffer())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_WithAppleTODOWriter_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithAppleTODOWriter(core.NewBuffer())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_DefaultAppleBuilderOptions_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultAppleBuilderOptions()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_DefaultAppleBuilderOptions_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultAppleBuilderOptions()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_DefaultAppleBuilderOptions_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultAppleBuilderOptions()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_AppleBuilder_Name_Good(t *core.T) {
- subject := &AppleBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleBuilder_Name_Bad(t *core.T) {
- subject := &AppleBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleBuilder_Name_Ugly(t *core.T) {
- subject := &AppleBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_AppleBuilder_Detect_Good(t *core.T) {
- subject := &AppleBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(coreio.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleBuilder_Detect_Bad(t *core.T) {
- subject := &AppleBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(coreio.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleBuilder_Detect_Ugly(t *core.T) {
- subject := &AppleBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(coreio.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_AppleBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &AppleBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &AppleBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &AppleBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_AppleBuilder_BuildWailsMacOS_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := NewAppleBuilder(WithAppleTODOWriter(nil))
- cfg := &build.Config{ProjectDir: t.TempDir()}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.BuildWailsMacOS(ctx, coreio.NewMemoryMedium(), cfg, core.Path(t.TempDir(), "go-build-compliance"), "agent", "amd64")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleBuilder_BuildWailsMacOS_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := NewAppleBuilder(WithAppleTODOWriter(nil))
- cfg := &build.Config{ProjectDir: t.TempDir()}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.BuildWailsMacOS(ctx, coreio.NewMemoryMedium(), cfg, "", "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleBuilder_BuildWailsMacOS_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := NewAppleBuilder(WithAppleTODOWriter(nil))
- cfg := &build.Config{ProjectDir: t.TempDir()}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.BuildWailsMacOS(ctx, coreio.NewMemoryMedium(), cfg, core.Path(t.TempDir(), "go-build-compliance"), "agent", "amd64")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_AppleBuilder_CreateUniversal_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &AppleBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.CreateUniversal(ctx, coreio.NewMemoryMedium(), coreio.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), "agent")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_AppleBuilder_CreateUniversal_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &AppleBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.CreateUniversal(ctx, coreio.NewMemoryMedium(), coreio.NewMemoryMedium(), "", "", "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_AppleBuilder_CreateUniversal_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &AppleBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.CreateUniversal(ctx, coreio.NewMemoryMedium(), coreio.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), "agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestApple_ValidateAppleOptions_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ValidateAppleOptions(AppleOptions{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestApple_ValidateAppleOptions_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ValidateAppleOptions(AppleOptions{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestApple_ValidateAppleOptions_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ValidateAppleOptions(AppleOptions{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/cpp.go b/pkg/build/builders/cpp.go
deleted file mode 100644
index 87a495b..0000000
--- a/pkg/build/builders/cpp.go
+++ /dev/null
@@ -1,539 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- stdfs "io/fs"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// CPPBuilder implements the Builder interface for C++ projects using CMake + Conan.
-// It wraps the Makefile-based build system from the .core/build submodule.
-//
-// b := builders.NewCPPBuilder()
-type CPPBuilder struct{}
-
-// NewCPPBuilder creates a new CPPBuilder instance.
-//
-// b := builders.NewCPPBuilder()
-func NewCPPBuilder() *CPPBuilder {
- return &CPPBuilder{}
-}
-
-// Name returns the builder's identifier.
-//
-// name := b.Name() // → "cpp"
-func (b *CPPBuilder) Name() string {
- return "cpp"
-}
-
-// Detect checks if this builder can handle the project (checks for CMakeLists.txt).
-//
-// ok, err := b.Detect(storage.Local, ".")
-func (b *CPPBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(build.IsCPPProject(fs, dir))
-}
-
-// Build compiles the C++ project using Make targets.
-// The build flow is: make configure → make build → make package.
-//
-// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
-func (b *CPPBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("CPPBuilder.Build", "config is nil", nil))
- }
-
- filesystem := cfg.FS
- if filesystem == nil {
- filesystem = storage.Local
- cfg.FS = filesystem
- }
- if cfg.OutputDir == "" {
- cfg.OutputDir = ax.Join(cfg.ProjectDir, "dist")
- }
-
- managedMake := b.hasManagedMakefile(filesystem, cfg.ProjectDir)
- if managedMake {
- // Managed C++ repos keep the Conan/CMake orchestration in the project Makefile.
- if valid := b.validateMake(); !valid.OK {
- return valid
- }
- if valid := b.validateConan(); !valid.OK {
- return valid
- }
- } else {
- if valid := b.validateCMake(); !valid.OK {
- return valid
- }
- if b.usesConan(filesystem, cfg.ProjectDir) {
- if valid := b.validateConan(); !valid.OK {
- return valid
- }
- }
- }
-
- // For C++ projects, the Makefile handles everything.
- // We don't iterate per-target like Go — the Makefile's configure + build
- // produces binaries for the host platform, and cross-compilation uses
- // named Conan profiles (e.g., make gcc-linux-armv8).
- if len(targets) == 0 {
- // Default to host platform
- targets = []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
- }
-
- var artifacts []build.Artifact
-
- for _, target := range targets {
- builtResult := b.buildTarget(ctx, cfg, target)
- if !builtResult.OK {
- return core.Fail(core.E("CPPBuilder.Build", "build failed", core.NewError(builtResult.Error())))
- }
- artifacts = append(artifacts, builtResult.Value.([]build.Artifact)...)
- }
-
- return core.Ok(artifacts)
-}
-
-// buildTarget compiles for a single target platform.
-func (b *CPPBuilder) buildTarget(ctx context.Context, cfg *build.Config, target build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("CPPBuilder.buildTarget", "config is nil", nil))
- }
- filesystem := cfg.FS
- if filesystem == nil {
- filesystem = storage.Local
- cfg.FS = filesystem
- }
- if !b.hasManagedMakefile(filesystem, cfg.ProjectDir) {
- return b.buildWithCMake(ctx, cfg, target)
- }
-
- // Determine if this is a cross-compile or host build
- isHostBuild := target.OS == runtime.GOOS && target.Arch == runtime.GOARCH
-
- if isHostBuild {
- return b.buildHost(ctx, cfg, target)
- }
-
- return b.buildCross(ctx, cfg, target)
-}
-
-// buildHost runs the standard make configure → make build → make package flow.
-func (b *CPPBuilder) buildHost(ctx context.Context, cfg *build.Config, target build.Target) core.Result {
- core.Print(nil, "Building C++ project for %s/%s (host)", target.OS, target.Arch)
-
- // Step 1: Configure (runs conan install + cmake configure)
- if ran := b.runMake(ctx, cfg, "configure"); !ran.OK {
- return core.Fail(core.E("CPPBuilder.buildHost", "configure failed", core.NewError(ran.Error())))
- }
-
- // Step 2: Build
- if ran := b.runMake(ctx, cfg, "build"); !ran.OK {
- return core.Fail(core.E("CPPBuilder.buildHost", "build failed", core.NewError(ran.Error())))
- }
-
- // Step 3: Package
- if ran := b.runMake(ctx, cfg, "package"); !ran.OK {
- return core.Fail(core.E("CPPBuilder.buildHost", "package failed", core.NewError(ran.Error())))
- }
-
- // Discover artifacts from build/packages/
- return b.findArtifacts(cfg.FS, cfg.ProjectDir, target)
-}
-
-// buildCross runs a cross-compilation using a Conan profile name.
-// The Makefile supports profile targets like: make gcc-linux-armv8
-func (b *CPPBuilder) buildCross(ctx context.Context, cfg *build.Config, target build.Target) core.Result {
- // Map target to a Conan profile name
- profile := b.targetToProfile(target)
- if profile == "" {
- return core.Fail(core.E("CPPBuilder.buildCross", "no Conan profile mapped for target "+target.OS+"/"+target.Arch, nil))
- }
-
- core.Print(nil, "Building C++ project for %s/%s (cross: %s)", target.OS, target.Arch, profile)
-
- // The Makefile exposes each profile as a top-level target
- if ran := b.runMake(ctx, cfg, profile); !ran.OK {
- return core.Fail(core.E("CPPBuilder.buildCross", "cross-compile for "+profile+" failed", core.NewError(ran.Error())))
- }
-
- return b.findArtifacts(cfg.FS, cfg.ProjectDir, target)
-}
-
-// buildWithCMake runs a generic CMake build for plain CMakeLists.txt projects.
-// Conan is used when the project declares a conanfile; otherwise the builder
-// configures CMake directly.
-func (b *CPPBuilder) buildWithCMake(ctx context.Context, cfg *build.Config, target build.Target) core.Result {
- filesystem := cfg.FS
- if filesystem == nil {
- filesystem = storage.Local
- cfg.FS = filesystem
- }
-
- platformDir := ax.Join(cfg.OutputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
- if created := filesystem.EnsureDir(platformDir); !created.OK {
- return core.Fail(core.E("CPPBuilder.buildWithCMake", "failed to create platform output directory", core.NewError(created.Error())))
- }
-
- buildDir := ax.Join(cfg.ProjectDir, "build", "cmake", core.Sprintf("%s_%s", target.OS, target.Arch))
- if created := filesystem.EnsureDir(buildDir); !created.OK {
- return core.Fail(core.E("CPPBuilder.buildWithCMake", "failed to create cmake build directory", core.NewError(created.Error())))
- }
-
- env := appendConfiguredEnv(cfg,
- core.Sprintf("GOOS=%s", target.OS),
- core.Sprintf("GOARCH=%s", target.Arch),
- core.Sprintf("TARGET_OS=%s", target.OS),
- core.Sprintf("TARGET_ARCH=%s", target.Arch),
- core.Sprintf("OUTPUT_DIR=%s", cfg.OutputDir),
- core.Sprintf("TARGET_DIR=%s", platformDir),
- )
- if cfg.CGO {
- env = append(env, "CGO_ENABLED=1")
- }
-
- useConan := b.usesConan(filesystem, cfg.ProjectDir)
- if useConan {
- if ran := b.runConanInstall(ctx, cfg, target, buildDir, env); !ran.OK {
- return ran
- }
- }
- if ran := b.runCMakeConfigure(ctx, cfg, target, buildDir, platformDir, useConan, env); !ran.OK {
- return ran
- }
- if ran := b.runCMakeBuild(ctx, cfg, buildDir, env); !ran.OK {
- return ran
- }
-
- artifacts := b.findGeneratedArtifacts(filesystem, platformDir, target)
- if len(artifacts) > 0 {
- return core.Ok(artifacts)
- }
-
- // Some generators ignore the explicit output directory and place binaries in
- // the build tree. Fall back to scanning the cmake build directory.
- artifacts = b.findGeneratedArtifacts(filesystem, buildDir, target)
- if len(artifacts) > 0 {
- return core.Ok(artifacts)
- }
-
- return core.Fail(core.E("CPPBuilder.buildWithCMake", "no build output found in "+platformDir+" or "+buildDir, nil))
-}
-
-// runMake executes a make target in the project directory.
-func (b *CPPBuilder) runMake(ctx context.Context, cfg *build.Config, target string) core.Result {
- makeCommandResult := b.resolveMakeCli()
- if !makeCommandResult.OK {
- return makeCommandResult
- }
- makeCommand := makeCommandResult.Value.(string)
-
- ran := ax.ExecWithEnv(ctx, cfg.ProjectDir, build.BuildEnvironment(cfg), makeCommand, target)
- if !ran.OK {
- return core.Fail(core.E("CPPBuilder.runMake", "make "+target+" failed", core.NewError(ran.Error())))
- }
- return core.Ok(nil)
-}
-
-func (b *CPPBuilder) runConanInstall(ctx context.Context, cfg *build.Config, target build.Target, buildDir string, env []string) core.Result {
- conanCommandResult := b.resolveConanCli()
- if !conanCommandResult.OK {
- return conanCommandResult
- }
- conanCommand := conanCommandResult.Value.(string)
-
- args := []string{"install", ".", "--output-folder", buildDir, "--build=missing"}
- if target.OS != runtime.GOOS || target.Arch != runtime.GOARCH {
- profile := b.targetToProfile(target)
- if profile == "" {
- return core.Fail(core.E("CPPBuilder.runConanInstall", "no Conan profile mapped for target "+target.OS+"/"+target.Arch, nil))
- }
- args = append(args, "--profile:host", profile)
- }
-
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, env, conanCommand, args...)
- if !output.OK {
- return core.Fail(core.E("CPPBuilder.runConanInstall", "conan install failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func (b *CPPBuilder) runCMakeConfigure(ctx context.Context, cfg *build.Config, target build.Target, buildDir, platformDir string, useConan bool, env []string) core.Result {
- cmakeCommandResult := b.resolveCMakeCli()
- if !cmakeCommandResult.OK {
- return cmakeCommandResult
- }
- cmakeCommand := cmakeCommandResult.Value.(string)
-
- args := []string{
- "-S", cfg.ProjectDir,
- "-B", buildDir,
- "-DCMAKE_BUILD_TYPE=Release",
- "-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=" + platformDir,
- "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + platformDir,
- "-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY=" + platformDir,
- }
- if useConan {
- args = append(args, "-DCMAKE_TOOLCHAIN_FILE="+ax.Join(buildDir, "conan_toolchain.cmake"))
- }
- if target.OS != runtime.GOOS || target.Arch != runtime.GOARCH {
- args = append(args, "-DCORE_TARGET="+target.OS+"/"+target.Arch)
- }
-
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, env, cmakeCommand, args...)
- if !output.OK {
- return core.Fail(core.E("CPPBuilder.runCMakeConfigure", "cmake configure failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func (b *CPPBuilder) runCMakeBuild(ctx context.Context, cfg *build.Config, buildDir string, env []string) core.Result {
- cmakeCommandResult := b.resolveCMakeCli()
- if !cmakeCommandResult.OK {
- return cmakeCommandResult
- }
- cmakeCommand := cmakeCommandResult.Value.(string)
-
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, env, cmakeCommand, "--build", buildDir, "--config", "Release")
- if !output.OK {
- return core.Fail(core.E("CPPBuilder.runCMakeBuild", "cmake build failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// findArtifacts searches for built packages in build/packages/.
-func (b *CPPBuilder) findArtifacts(fs storage.Medium, projectDir string, target build.Target) core.Result {
- packagesDir := ax.Join(projectDir, "build", "packages")
-
- if !fs.IsDir(packagesDir) {
- // Fall back to searching build/release/src/ for raw binaries
- return b.findBinaries(fs, projectDir, target)
- }
-
- entriesResult := fs.List(packagesDir)
- if !entriesResult.OK {
- return core.Fail(core.E("CPPBuilder.findArtifacts", "failed to list packages directory", core.NewError(entriesResult.Error())))
- }
- entries := entriesResult.Value.([]stdfs.DirEntry)
-
- var artifacts []build.Artifact
- for _, entry := range entries {
- if entry.IsDir() {
- continue
- }
-
- name := entry.Name()
- // Skip checksum files and hidden files
- if core.HasSuffix(name, ".sha256") || core.HasPrefix(name, ".") {
- continue
- }
-
- artifacts = append(artifacts, build.Artifact{
- Path: ax.Join(packagesDir, name),
- OS: target.OS,
- Arch: target.Arch,
- })
- }
-
- return core.Ok(artifacts)
-}
-
-// findBinaries searches for compiled binaries in build/release/src/.
-func (b *CPPBuilder) findBinaries(fs storage.Medium, projectDir string, target build.Target) core.Result {
- binDir := ax.Join(projectDir, "build", "release", "src")
-
- if !fs.IsDir(binDir) {
- return core.Fail(core.E("CPPBuilder.findBinaries", "no build output found in "+binDir, nil))
- }
-
- return core.Ok(b.findGeneratedArtifacts(fs, binDir, target))
-}
-
-func (b *CPPBuilder) findGeneratedArtifacts(fs storage.Medium, dir string, target build.Target) []build.Artifact {
- if !fs.IsDir(dir) {
- return nil
- }
-
- entriesResult := fs.List(dir)
- if !entriesResult.OK {
- return nil
- }
- entries := entriesResult.Value.([]stdfs.DirEntry)
-
- var artifacts []build.Artifact
- for _, entry := range entries {
- if entry.IsDir() {
- if target.OS == "darwin" && core.HasSuffix(entry.Name(), ".app") {
- artifacts = append(artifacts, build.Artifact{
- Path: ax.Join(dir, entry.Name()),
- OS: target.OS,
- Arch: target.Arch,
- })
- }
- continue
- }
-
- name := entry.Name()
- // Skip common build metadata and non-runtime artefacts.
- if core.HasPrefix(name, ".") ||
- core.HasPrefix(name, "CMake") ||
- core.HasPrefix(name, "cmake") ||
- core.HasPrefix(name, "conan") ||
- core.HasSuffix(name, ".a") ||
- core.HasSuffix(name, ".o") ||
- core.HasSuffix(name, ".cmake") ||
- core.HasSuffix(name, ".ninja") ||
- core.HasSuffix(name, ".txt") ||
- name == "Makefile" {
- continue
- }
-
- fullPath := ax.Join(dir, name)
-
- // On Unix, check if file is executable
- if target.OS != "windows" {
- info := fs.Stat(fullPath)
- if !info.OK {
- continue
- }
- if info.Value.(stdfs.FileInfo).Mode()&0111 == 0 {
- continue
- }
- }
-
- artifacts = append(artifacts, build.Artifact{
- Path: fullPath,
- OS: target.OS,
- Arch: target.Arch,
- })
- }
-
- return artifacts
-}
-
-// targetToProfile maps a build target to a Conan cross-compilation profile name.
-// Profile names match those in .core/build/cmake/profiles/.
-func (b *CPPBuilder) targetToProfile(target build.Target) string {
- key := target.OS + "/" + target.Arch
- profiles := map[string]string{
- "linux/amd64": "gcc-linux-x86_64",
- "linux/x86_64": "gcc-linux-x86_64",
- "linux/arm64": "gcc-linux-armv8",
- "linux/armv8": "gcc-linux-armv8",
- "darwin/arm64": "apple-clang-armv8",
- "darwin/armv8": "apple-clang-armv8",
- "darwin/amd64": "apple-clang-x86_64",
- "darwin/x86_64": "apple-clang-x86_64",
- "windows/amd64": "msvc-194-x86_64",
- "windows/x86_64": "msvc-194-x86_64",
- }
-
- return profiles[key]
-}
-
-// validateMake checks if make is available.
-func (b *CPPBuilder) validateMake() core.Result {
- return b.resolveMakeCli()
-}
-
-// validateConan checks if conan is available.
-func (b *CPPBuilder) validateConan() core.Result {
- return b.resolveConanCli()
-}
-
-// validateCMake checks if cmake is available.
-func (b *CPPBuilder) validateCMake() core.Result {
- return b.resolveCMakeCli()
-}
-
-// resolveMakeCli returns the executable path for make or gmake.
-func (b *CPPBuilder) resolveMakeCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/bin/make",
- "/usr/local/bin/make",
- "/opt/homebrew/bin/make",
- "/usr/local/bin/gmake",
- "/opt/homebrew/bin/gmake",
- }
- }
-
- command := ax.ResolveCommand("make", paths...)
- if !command.OK {
- return core.Fail(core.E("CPPBuilder.resolveMakeCli", "make not found. Install build-essential (Linux) or Xcode Command Line Tools (macOS)", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// resolveConanCli returns the executable path for conan.
-func (b *CPPBuilder) resolveConanCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/conan",
- "/opt/homebrew/bin/conan",
- }
-
- if home := core.Env("HOME"); home != "" {
- paths = append(paths, ax.Join(home, ".local", "bin", "conan"))
- }
- }
-
- command := ax.ResolveCommand("conan", paths...)
- if !command.OK {
- return core.Fail(core.E("CPPBuilder.resolveConanCli", "conan not found. Install it with: python -m pip install conan", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// resolveCMakeCli returns the executable path for cmake.
-func (b *CPPBuilder) resolveCMakeCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/bin/cmake",
- "/usr/local/bin/cmake",
- "/opt/homebrew/bin/cmake",
- }
- }
-
- command := ax.ResolveCommand("cmake", paths...)
- if !command.OK {
- return core.Fail(core.E("CPPBuilder.resolveCMakeCli", "cmake not found. Install it with: brew install cmake or apt-get install cmake", core.NewError(command.Error())))
- }
-
- return command
-}
-
-func (b *CPPBuilder) hasManagedMakefile(fs storage.Medium, dir string) bool {
- if fs == nil {
- fs = storage.Local
- }
-
- for _, name := range []string{"Makefile", "GNUmakefile", "makefile"} {
- if fs.IsFile(ax.Join(dir, name)) {
- return true
- }
- }
-
- return false
-}
-
-func (b *CPPBuilder) usesConan(fs storage.Medium, dir string) bool {
- if fs == nil {
- fs = storage.Local
- }
-
- return fs.IsFile(ax.Join(dir, "conanfile.py")) || fs.IsFile(ax.Join(dir, "conanfile.txt"))
-}
-
-// Ensure CPPBuilder implements the Builder interface.
-var _ build.Builder = (*CPPBuilder)(nil)
diff --git a/pkg/build/builders/cpp_example_test.go b/pkg/build/builders/cpp_example_test.go
deleted file mode 100644
index c5b9bd9..0000000
--- a/pkg/build/builders/cpp_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewCPPBuilder references NewCPPBuilder on this package API surface.
-func ExampleNewCPPBuilder() {
- _ = NewCPPBuilder
- core.Println("NewCPPBuilder")
- // Output: NewCPPBuilder
-}
-
-// ExampleCPPBuilder_Name references CPPBuilder.Name on this package API surface.
-func ExampleCPPBuilder_Name() {
- _ = (*CPPBuilder).Name
- core.Println("CPPBuilder.Name")
- // Output: CPPBuilder.Name
-}
-
-// ExampleCPPBuilder_Detect references CPPBuilder.Detect on this package API surface.
-func ExampleCPPBuilder_Detect() {
- _ = (*CPPBuilder).Detect
- core.Println("CPPBuilder.Detect")
- // Output: CPPBuilder.Detect
-}
-
-// ExampleCPPBuilder_Build references CPPBuilder.Build on this package API surface.
-func ExampleCPPBuilder_Build() {
- _ = (*CPPBuilder).Build
- core.Println("CPPBuilder.Build")
- // Output: CPPBuilder.Build
-}
diff --git a/pkg/build/builders/cpp_test.go b/pkg/build/builders/cpp_test.go
deleted file mode 100644
index 55f12c7..0000000
--- a/pkg/build/builders/cpp_test.go
+++ /dev/null
@@ -1,677 +0,0 @@
-package builders
-
-import (
- "context"
- "runtime"
- "testing"
-
- "dappco.re/go/build/internal/ax"
-
- core "dappco.re/go"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func setupFakeCPPCommand(t *testing.T, binDir, name, script string) {
- t.Helper()
- if result := ax.WriteFile(ax.Join(binDir, name), []byte(script), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func requireCPPBool(t *testing.T, result core.Result) bool {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(bool)
-}
-
-func requireCPPArtifacts(t *testing.T, result core.Result) []build.Artifact {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]build.Artifact)
-}
-
-func requireCPPString(t *testing.T, result core.Result) string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func requireBuilderBytes(t *testing.T, result core.Result) []byte {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]byte)
-}
-
-func cppCrossTarget() build.Target {
- switch runtime.GOOS {
- case "darwin":
- if runtime.GOARCH == "arm64" {
- return build.Target{OS: "darwin", Arch: "amd64"}
- }
- return build.Target{OS: "darwin", Arch: "arm64"}
- case "linux":
- if runtime.GOARCH == "arm64" {
- return build.Target{OS: "linux", Arch: "amd64"}
- }
- return build.Target{OS: "linux", Arch: "arm64"}
- default:
- return build.Target{OS: "linux", Arch: "amd64"}
- }
-}
-
-func TestCPP_CPPBuilderNameGood(t *testing.T) {
- builder := NewCPPBuilder()
- if !stdlibAssertEqual("cpp", builder.Name()) {
- t.Fatalf("want %v, got %v", "cpp", builder.Name())
- }
-
-}
-
-func TestCPP_CPPBuilderDetectGood(t *testing.T) {
- fs := storage.Local
-
- t.Run("detects C++ project with CMakeLists.txt", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "CMakeLists.txt"), []byte("cmake_minimum_required(VERSION 3.16)"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewCPPBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for non-C++ project", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewCPPBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false for empty directory", func(t *testing.T) {
- dir := t.TempDir()
-
- builder := NewCPPBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestCPP_CPPBuilderBuildBad(t *testing.T) {
- t.Run("returns error for nil config", func(t *testing.T) {
- builder := NewCPPBuilder()
- result := builder.Build(nil, nil, []build.Target{{OS: "linux", Arch: "amd64"}})
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "config is nil") {
- t.Fatalf("expected %v to contain %v", result.Error(), "config is nil")
- }
-
- })
-}
-
-func TestCPP_CPPBuilderBuildGood(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("C++ builder command fixtures use POSIX shell scripts")
- }
-
- t.Run("preserves the managed Makefile pipeline when present", func(t *testing.T) {
- projectDir := t.TempDir()
- binDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "make.log")
- if result := ax.WriteFile(ax.Join(projectDir, "CMakeLists.txt"), []byte("cmake_minimum_required(VERSION 3.16)\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result := ax.WriteFile(ax.Join(projectDir, "Makefile"), []byte("all:\n\t@true\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- setupFakeCPPCommand(t, binDir, "make", `#!/bin/sh
-set -eu
-printf 'make %s\n' "$*" >> "${CPP_BUILD_LOG_FILE}"
-case "${1:-}" in
- configure|build)
- exit 0
- ;;
- package)
- mkdir -p build/packages
- printf 'pkg\n' > build/packages/test-1.0.tar.gz
- exit 0
- ;;
-esac
-exit 1
-`)
- setupFakeCPPCommand(t, binDir, "conan", `#!/bin/sh
-set -eu
-printf 'conan %s\n' "$*" >> "${CPP_BUILD_LOG_FILE}"
-exit 0
-`)
-
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
- t.Setenv("CPP_BUILD_LOG_FILE", logPath)
-
- builder := NewCPPBuilder()
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: ax.Join(projectDir, "dist"),
- Name: "testapp",
- }, []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "build", "packages", "test-1.0.tar.gz"), artifacts[0].Path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "build", "packages", "test-1.0.tar.gz"), artifacts[0].Path)
- }
-
- content := requireCPPString(t, storage.Local.Read(logPath))
- if !stdlibAssertContains(content, "make configure") {
- t.Fatalf("expected %v to contain %v", content, "make configure")
- }
- if !stdlibAssertContains(content, "make build") {
- t.Fatalf("expected %v to contain %v", content, "make build")
- }
- if !stdlibAssertContains(content, "make package") {
- t.Fatalf("expected %v to contain %v", content, "make package")
- }
- if stdlibAssertContains(content, "cmake ") {
- t.Fatalf("expected %v not to contain %v", content, "cmake ")
- }
-
- })
-
- t.Run("falls back to plain cmake for generic CMake projects", func(t *testing.T) {
- projectDir := t.TempDir()
- binDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "cmake.log")
- statePath := ax.Join(t.TempDir(), "cmake-state")
- if result := ax.WriteFile(ax.Join(projectDir, "CMakeLists.txt"), []byte("cmake_minimum_required(VERSION 3.16)\nproject(demo)\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- setupFakeCPPCommand(t, binDir, "cmake", `#!/bin/sh
-set -eu
-printf 'cmake %s\n' "$*" >> "${CPP_BUILD_LOG_FILE}"
-if [ "${1:-}" = "-S" ]; then
- for arg in "$@"; do
- case "$arg" in
- -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=*)
- printf '%s\n' "${arg#*=}" > "${CPP_CMAKE_STATE_FILE}"
- ;;
- esac
- done
- exit 0
-fi
-if [ "${1:-}" = "--build" ]; then
- runtime_dir="$(cat "${CPP_CMAKE_STATE_FILE}")"
- mkdir -p "${runtime_dir}"
- printf 'binary\n' > "${runtime_dir}/${NAME:-testapp}"
- chmod +x "${runtime_dir}/${NAME:-testapp}"
- exit 0
-fi
-exit 1
-`)
-
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
- t.Setenv("CPP_BUILD_LOG_FILE", logPath)
- t.Setenv("CPP_CMAKE_STATE_FILE", statePath)
-
- target := build.Target{OS: runtime.GOOS, Arch: runtime.GOARCH}
- builder := NewCPPBuilder()
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: ax.Join(projectDir, "dist"),
- Name: "testapp",
- }, []build.Target{target}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", target.OS+"_"+target.Arch, "testapp"), artifacts[0].Path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", target.OS+"_"+target.Arch, "testapp"), artifacts[0].Path)
- }
-
- content := requireCPPString(t, storage.Local.Read(logPath))
- if !stdlibAssertContains(content, "cmake -S") {
- t.Fatalf("expected %v to contain %v", content, "cmake -S")
- }
- if !stdlibAssertContains(content, "cmake --build") {
- t.Fatalf("expected %v to contain %v", content, "cmake --build")
- }
- if stdlibAssertContains(content, "conan ") {
- t.Fatalf("expected %v not to contain %v", content, "conan ")
- }
- if stdlibAssertContains(content, "make configure") {
- t.Fatalf("expected %v not to contain %v", content, "make configure")
- }
- if stdlibAssertContains(content, "make build") {
- t.Fatalf("expected %v not to contain %v", content, "make build")
- }
- if stdlibAssertContains(content, "make package") {
- t.Fatalf("expected %v not to contain %v", content, "make package")
- }
-
- })
-
- t.Run("uses conan plus cmake for generic cross-builds when a conanfile exists", func(t *testing.T) {
- projectDir := t.TempDir()
- binDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "conan-cmake.log")
- statePath := ax.Join(t.TempDir(), "conan-cmake-state")
- if result := ax.WriteFile(ax.Join(projectDir, "CMakeLists.txt"), []byte("cmake_minimum_required(VERSION 3.16)\nproject(demo)\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result := ax.WriteFile(ax.Join(projectDir, "conanfile.txt"), []byte("[requires]\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- setupFakeCPPCommand(t, binDir, "conan", `#!/bin/sh
-set -eu
-printf 'conan %s\n' "$*" >> "${CPP_BUILD_LOG_FILE}"
-output_dir=""
-while [ "$#" -gt 0 ]; do
- if [ "$1" = "--output-folder" ]; then
- output_dir="$2"
- shift 2
- continue
- fi
- shift
-done
-mkdir -p "${output_dir}"
-printf '# toolchain\n' > "${output_dir}/conan_toolchain.cmake"
-`)
- setupFakeCPPCommand(t, binDir, "cmake", `#!/bin/sh
-set -eu
-printf 'cmake %s\n' "$*" >> "${CPP_BUILD_LOG_FILE}"
-if [ "${1:-}" = "-S" ]; then
- for arg in "$@"; do
- case "$arg" in
- -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=*)
- printf '%s\n' "${arg#*=}" > "${CPP_CMAKE_STATE_FILE}"
- ;;
- esac
- done
- exit 0
-fi
-if [ "${1:-}" = "--build" ]; then
- runtime_dir="$(cat "${CPP_CMAKE_STATE_FILE}")"
- mkdir -p "${runtime_dir}"
- printf 'binary\n' > "${runtime_dir}/${NAME:-testapp}"
- chmod +x "${runtime_dir}/${NAME:-testapp}"
- exit 0
-fi
-exit 1
-`)
-
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
- t.Setenv("CPP_BUILD_LOG_FILE", logPath)
- t.Setenv("CPP_CMAKE_STATE_FILE", statePath)
-
- target := cppCrossTarget()
- builder := NewCPPBuilder()
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: ax.Join(projectDir, "dist"),
- Name: "testapp",
- }, []build.Target{target}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", target.OS+"_"+target.Arch, "testapp"), artifacts[0].Path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", target.OS+"_"+target.Arch, "testapp"), artifacts[0].Path)
- }
-
- content := requireCPPString(t, storage.Local.Read(logPath))
- if !stdlibAssertContains(content, "conan install . --output-folder "+ax.Join(projectDir, "build", "cmake", target.OS+"_"+target.Arch)+" --build=missing --profile:host "+builder.targetToProfile(target)) {
- t.Fatalf("expected %v to contain %v", content, "conan install . --output-folder "+ax.Join(projectDir, "build", "cmake", target.OS+"_"+target.Arch)+" --build=missing --profile:host "+builder.targetToProfile(target))
- }
- if !stdlibAssertContains(content, "cmake -S") {
- t.Fatalf("expected %v to contain %v", content, "cmake -S")
- }
- if !stdlibAssertContains(content, "-DCMAKE_TOOLCHAIN_FILE="+ax.Join(projectDir, "build", "cmake", target.OS+"_"+target.Arch, "conan_toolchain.cmake")) {
- t.Fatalf("expected %v to contain %v", content, "-DCMAKE_TOOLCHAIN_FILE="+ax.Join(projectDir, "build", "cmake", target.OS+"_"+target.Arch, "conan_toolchain.cmake"))
- }
- if !stdlibAssertContains(content, "cmake --build") {
- t.Fatalf("expected %v to contain %v", content, "cmake --build")
- }
- if stdlibAssertContains(content, "make configure") {
- t.Fatalf("expected %v not to contain %v", content, "make configure")
- }
- if stdlibAssertContains(content, "make build") {
- t.Fatalf("expected %v not to contain %v", content, "make build")
- }
- if stdlibAssertContains(content, "make package") {
- t.Fatalf("expected %v not to contain %v", content, "make package")
- }
-
- })
-}
-
-func TestCPP_CPPBuilderTargetToProfileGood(t *testing.T) {
- builder := NewCPPBuilder()
-
- tests := []struct {
- os, arch string
- expected string
- }{
- {"linux", "amd64", "gcc-linux-x86_64"},
- {"linux", "x86_64", "gcc-linux-x86_64"},
- {"linux", "arm64", "gcc-linux-armv8"},
- {"darwin", "arm64", "apple-clang-armv8"},
- {"darwin", "amd64", "apple-clang-x86_64"},
- {"windows", "amd64", "msvc-194-x86_64"},
- }
-
- for _, tt := range tests {
- t.Run(tt.os+"/"+tt.arch, func(t *testing.T) {
- profile := builder.targetToProfile(build.Target{OS: tt.os, Arch: tt.arch})
- if !stdlibAssertEqual(tt.expected, profile) {
- t.Fatalf("want %v, got %v", tt.expected, profile)
- }
-
- })
- }
-}
-
-func TestCPP_CPPBuilderTargetToProfileBad(t *testing.T) {
- builder := NewCPPBuilder()
-
- t.Run("returns empty for unknown target", func(t *testing.T) {
- profile := builder.targetToProfile(build.Target{OS: "plan9", Arch: "mips"})
- if !stdlibAssertEmpty(profile) {
- t.Fatalf("expected empty, got %v", profile)
- }
-
- })
-}
-
-func TestCPP_CPPBuilderFindArtifactsGood(t *testing.T) {
- fs := storage.Local
-
- t.Run("finds packages in build/packages", func(t *testing.T) {
- dir := t.TempDir()
- packagesDir := ax.Join(dir, "build", "packages")
- if result := ax.MkdirAll(packagesDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result := ax.WriteFile(ax.Join(packagesDir, "test-1.0-linux-x86_64.tar.xz"), []byte("pkg"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result := ax.WriteFile(ax.Join(packagesDir, "test-1.0-linux-x86_64.tar.xz.sha256"), []byte("checksum"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result := ax.WriteFile(ax.Join(packagesDir, "test-1.0-linux-x86_64.rpm"), []byte("rpm"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewCPPBuilder()
- target := build.Target{OS: "linux", Arch: "amd64"}
- artifacts := requireCPPArtifacts(t, builder.findArtifacts(fs, dir, target))
- if len(artifacts) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(artifacts))
- }
-
- for _, a := range artifacts {
- if !stdlibAssertEqual("linux", a.OS) {
- t.Fatalf("want %v, got %v", "linux", a.OS)
- }
- if !stdlibAssertEqual("amd64", a.Arch) {
- t.Fatalf("want %v, got %v", "amd64", a.Arch)
- }
- if ax.Ext(a.Path) == ".sha256" {
- t.Fatal("expected false")
- }
-
- }
- })
-
- t.Run("falls back to binaries in build/release/src", func(t *testing.T) {
- dir := t.TempDir()
- binDir := ax.Join(dir, "build", "release", "src")
- if result := ax.MkdirAll(binDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- binPath := ax.Join(binDir, "test-daemon")
- if result := ax.WriteFile(binPath, []byte("binary"), 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result := ax.WriteFile(ax.Join(binDir, "libcrypto.a"), []byte("lib"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewCPPBuilder()
- target := build.Target{OS: "linux", Arch: "amd64"}
- artifacts := requireCPPArtifacts(t, builder.findArtifacts(fs, dir, target))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertContains(artifacts[0].Path, "test-daemon") {
- t.Fatalf("expected %v to contain %v", artifacts[0].Path, "test-daemon")
- }
-
- })
-}
-
-func TestCPP_CPPBuilderResolveMakeCliGood(t *testing.T) {
- builder := NewCPPBuilder()
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "make")
- if result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- command := requireCPPString(t, builder.resolveMakeCli(fallbackPath))
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestCPP_CPPBuilderResolveMakeCliBad(t *testing.T) {
- builder := NewCPPBuilder()
- t.Setenv("PATH", "")
-
- result := builder.resolveMakeCli(ax.Join(t.TempDir(), "missing-make"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "make not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "make not found")
- }
-
-}
-
-func TestCPP_CPPBuilderResolveConanCliGood(t *testing.T) {
- builder := NewCPPBuilder()
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "conan")
- if result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- command := requireCPPString(t, builder.resolveConanCli(fallbackPath))
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestCPP_CPPBuilderResolveConanCliBad(t *testing.T) {
- builder := NewCPPBuilder()
- t.Setenv("PATH", "")
-
- result := builder.resolveConanCli(ax.Join(t.TempDir(), "missing-conan"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "conan not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "conan not found")
- }
-
-}
-
-func TestCPP_CPPBuilderInterfaceGood(t *testing.T) {
- builder := NewCPPBuilder()
- var _ build.Builder = builder
- if !stdlibAssertEqual("cpp", builder.Name()) {
- t.Fatalf("want %v, got %v", "cpp", builder.Name())
- }
- detected := requireCPPBool(t, builder.Detect(nil, t.TempDir()))
- if detected {
- t.Fatal("expected empty temp directory not to be detected")
- }
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCpp_NewCPPBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewCPPBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCpp_NewCPPBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewCPPBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCpp_NewCPPBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewCPPBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCpp_CPPBuilder_Name_Good(t *core.T) {
- subject := &CPPBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCpp_CPPBuilder_Name_Bad(t *core.T) {
- subject := &CPPBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCpp_CPPBuilder_Name_Ugly(t *core.T) {
- subject := &CPPBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCpp_CPPBuilder_Detect_Good(t *core.T) {
- subject := &CPPBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCpp_CPPBuilder_Detect_Bad(t *core.T) {
- subject := &CPPBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCpp_CPPBuilder_Detect_Ugly(t *core.T) {
- subject := &CPPBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCpp_CPPBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &CPPBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCpp_CPPBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &CPPBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCpp_CPPBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &CPPBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/deno.go b/pkg/build/builders/deno.go
deleted file mode 100644
index 3fe3ed5..0000000
--- a/pkg/build/builders/deno.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package builders
-
-import (
- "unicode"
-
- "dappco.re/go"
- "dappco.re/go/build/pkg/build"
-)
-
-type commandSpec struct {
- command string
- args []string
-}
-
-// resolveDenoBuildCommand returns the Deno build invocation using the action-style
-// environment override first, then the persisted build config, then the default task.
-func resolveDenoBuildCommand(cfg *build.Config, resolveDeno func(...string) core.Result) core.Result {
- override := core.Trim(core.Env("DENO_BUILD"))
- if override == "" && cfg != nil {
- override = core.Trim(cfg.DenoBuild)
- }
- if override != "" {
- argsResult := splitCommandLine(override)
- if !argsResult.OK {
- return core.Fail(core.E("builders.resolveDenoBuildCommand", "invalid DENO_BUILD command", core.NewError(argsResult.Error())))
- }
- args := argsResult.Value.([]string)
- if len(args) == 0 {
- return core.Fail(core.E("builders.resolveDenoBuildCommand", "DENO_BUILD command is empty", nil))
- }
- return core.Ok(commandSpec{command: args[0], args: args[1:]})
- }
-
- command := resolveDeno()
- if !command.OK {
- return command
- }
- return core.Ok(commandSpec{command: command.Value.(string), args: []string{"task", "build"}})
-}
-
-// resolveNpmBuildCommand returns the npm build invocation using the action-style
-// environment override first, then the persisted build config, then the default task.
-func resolveNpmBuildCommand(cfg *build.Config, resolveNpm func(...string) core.Result) core.Result {
- override := core.Trim(core.Env("NPM_BUILD"))
- if override == "" && cfg != nil {
- override = core.Trim(cfg.NpmBuild)
- }
- if override != "" {
- argsResult := splitCommandLine(override)
- if !argsResult.OK {
- return core.Fail(core.E("builders.resolveNpmBuildCommand", "invalid NPM_BUILD command", core.NewError(argsResult.Error())))
- }
- args := argsResult.Value.([]string)
- if len(args) == 0 {
- return core.Fail(core.E("builders.resolveNpmBuildCommand", "NPM_BUILD command is empty", nil))
- }
- return core.Ok(commandSpec{command: args[0], args: args[1:]})
- }
-
- command := resolveNpm()
- if !command.OK {
- return command
- }
- return core.Ok(commandSpec{command: command.Value.(string), args: []string{"run", "build"}})
-}
-
-// splitCommandLine tokenises a command string with basic shell-style quoting.
-func splitCommandLine(command string) core.Result {
- command = core.Trim(command)
- if command == "" {
- return core.Ok([]string(nil))
- }
-
- var (
- args []string
- quote rune
- escape bool
- )
- current := core.NewBuilder()
-
- flush := func() {
- if current.Len() == 0 {
- return
- }
- args = append(args, current.String())
- current.Reset()
- }
-
- for _, r := range command {
- switch {
- case escape:
- current.WriteRune(r)
- escape = false
- case r == '\\' && quote != '\'':
- escape = true
- case quote != 0:
- if r == quote {
- quote = 0
- continue
- }
- current.WriteRune(r)
- case r == '"' || r == '\'':
- quote = r
- case unicode.IsSpace(r):
- flush()
- default:
- current.WriteRune(r)
- }
- }
-
- if escape {
- current.WriteRune('\\')
- }
- if quote != 0 {
- return core.Fail(core.E("builders.splitCommandLine", "unterminated quote in command", nil))
- }
-
- flush()
- return core.Ok(args)
-}
diff --git a/pkg/build/builders/deno_test.go b/pkg/build/builders/deno_test.go
deleted file mode 100644
index eac4ec8..0000000
--- a/pkg/build/builders/deno_test.go
+++ /dev/null
@@ -1,263 +0,0 @@
-package builders
-
-import (
- core "dappco.re/go"
- "testing"
-
- "dappco.re/go/build/pkg/build"
-)
-
-func requireDenoCommandSpec(t *testing.T, result core.Result) commandSpec {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(commandSpec)
-}
-
-func requireDenoArgs(t *testing.T, result core.Result) []string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]string)
-}
-
-func TestDeno_ResolveDenoBuildCommandGood(t *testing.T) {
- t.Run("environment override takes precedence over config and default", func(t *testing.T) {
- t.Setenv("DENO_BUILD", `deno task "build docs" --watch`)
-
- cfg := &build.Config{DenoBuild: "deno task ignored"}
-
- spec := requireDenoCommandSpec(t, resolveDenoBuildCommand(cfg, func(...string) core.Result {
- t.Fatal("resolver should not be called when DENO_BUILD is set")
- return core.Ok("")
- }))
- if !stdlibAssertEqual("deno", spec.command) {
- t.Fatalf("want %v, got %v", "deno", spec.command)
- }
- if !stdlibAssertEqual([]string{"task", "build docs", "--watch"}, spec.args) {
- t.Fatalf("want %v, got %v", []string{"task", "build docs", "--watch"}, spec.args)
- }
-
- })
-
- t.Run("config override is used when environment override is absent", func(t *testing.T) {
- t.Setenv("DENO_BUILD", "")
-
- cfg := &build.Config{DenoBuild: `deno task "bundle app"`}
-
- spec := requireDenoCommandSpec(t, resolveDenoBuildCommand(cfg, func(...string) core.Result {
- t.Fatal("resolver should not be called when config override is set")
- return core.Ok("")
- }))
- if !stdlibAssertEqual("deno", spec.command) {
- t.Fatalf("want %v, got %v", "deno", spec.command)
- }
- if !stdlibAssertEqual([]string{"task", "bundle app"}, spec.args) {
- t.Fatalf("want %v, got %v", []string{"task", "bundle app"}, spec.args)
- }
-
- })
-
- t.Run("falls back to the resolver default when no override exists", func(t *testing.T) {
- t.Setenv("DENO_BUILD", "")
-
- spec := requireDenoCommandSpec(t, resolveDenoBuildCommand(&build.Config{}, func(...string) core.Result {
- return core.Ok("deno")
- }))
- if !stdlibAssertEqual("deno", spec.command) {
- t.Fatalf("want %v, got %v", "deno", spec.command)
- }
- if !stdlibAssertEqual([]string{"task", "build"}, spec.args) {
- t.Fatalf("want %v, got %v", []string{"task", "build"}, spec.args)
- }
-
- })
-}
-
-func TestDeno_ResolveDenoBuildCommandBad(t *testing.T) {
- t.Run("invalid shell quoting is rejected", func(t *testing.T) {
- t.Setenv("DENO_BUILD", `deno task "unterminated`)
-
- result := resolveDenoBuildCommand(&build.Config{}, func(...string) core.Result {
- t.Fatal("resolver should not be called when parsing fails")
- return core.Ok("")
- })
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "invalid DENO_BUILD command") {
- t.Fatalf("expected %v to contain %v", result.Error(), "invalid DENO_BUILD command")
- }
-
- })
-
- t.Run("resolver errors are surfaced when no override exists", func(t *testing.T) {
- t.Setenv("DENO_BUILD", "")
-
- result := resolveDenoBuildCommand(nil, func(...string) core.Result {
- return core.Fail(core.NewError("deno not found"))
- })
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "deno not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "deno not found")
- }
-
- })
-}
-
-func TestDeno_ResolveDenoBuildCommandUgly(t *testing.T) {
- t.Run("trimmed empty command falls through to the default resolver", func(t *testing.T) {
- t.Setenv("DENO_BUILD", " ")
-
- spec := requireDenoCommandSpec(t, resolveDenoBuildCommand(&build.Config{}, func(...string) core.Result {
- return core.Ok("deno")
- }))
- if !stdlibAssertEqual("deno", spec.command) {
- t.Fatalf("want %v, got %v", "deno", spec.command)
- }
- if !stdlibAssertEqual([]string{"task", "build"}, spec.args) {
- t.Fatalf("want %v, got %v", []string{"task", "build"}, spec.args)
- }
-
- })
-}
-
-func TestDeno_ResolveNpmBuildCommandGood(t *testing.T) {
- t.Run("environment override takes precedence over config and default", func(t *testing.T) {
- t.Setenv("NPM_BUILD", `npm run "build docs" -- --watch`)
-
- cfg := &build.Config{NpmBuild: "npm run ignored"}
-
- spec := requireDenoCommandSpec(t, resolveNpmBuildCommand(cfg, func(...string) core.Result {
- t.Fatal("resolver should not be called when NPM_BUILD is set")
- return core.Ok("")
- }))
- if !stdlibAssertEqual("npm", spec.command) {
- t.Fatalf("want %v, got %v", "npm", spec.command)
- }
- if !stdlibAssertEqual([]string{"run", "build docs", "--", "--watch"}, spec.args) {
- t.Fatalf("want %v, got %v", []string{"run", "build docs", "--", "--watch"}, spec.args)
- }
-
- })
-
- t.Run("config override is used when environment override is absent", func(t *testing.T) {
- t.Setenv("NPM_BUILD", "")
-
- cfg := &build.Config{NpmBuild: `npm run "bundle app"`}
-
- spec := requireDenoCommandSpec(t, resolveNpmBuildCommand(cfg, func(...string) core.Result {
- t.Fatal("resolver should not be called when config override is set")
- return core.Ok("")
- }))
- if !stdlibAssertEqual("npm", spec.command) {
- t.Fatalf("want %v, got %v", "npm", spec.command)
- }
- if !stdlibAssertEqual([]string{"run", "bundle app"}, spec.args) {
- t.Fatalf("want %v, got %v", []string{"run", "bundle app"}, spec.args)
- }
-
- })
-
- t.Run("falls back to the resolver default when no override exists", func(t *testing.T) {
- t.Setenv("NPM_BUILD", "")
-
- spec := requireDenoCommandSpec(t, resolveNpmBuildCommand(&build.Config{}, func(...string) core.Result {
- return core.Ok("npm")
- }))
- if !stdlibAssertEqual("npm", spec.command) {
- t.Fatalf("want %v, got %v", "npm", spec.command)
- }
- if !stdlibAssertEqual([]string{"run", "build"}, spec.args) {
- t.Fatalf("want %v, got %v", []string{"run", "build"}, spec.args)
- }
-
- })
-}
-
-func TestDeno_ResolveNpmBuildCommandBad(t *testing.T) {
- t.Run("invalid shell quoting is rejected", func(t *testing.T) {
- t.Setenv("NPM_BUILD", `npm run "unterminated`)
-
- result := resolveNpmBuildCommand(&build.Config{}, func(...string) core.Result {
- t.Fatal("resolver should not be called when parsing fails")
- return core.Ok("")
- })
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "invalid NPM_BUILD command") {
- t.Fatalf("expected %v to contain %v", result.Error(), "invalid NPM_BUILD command")
- }
-
- })
-
- t.Run("resolver errors are surfaced when no override exists", func(t *testing.T) {
- t.Setenv("NPM_BUILD", "")
-
- result := resolveNpmBuildCommand(nil, func(...string) core.Result {
- return core.Fail(core.NewError("npm not found"))
- })
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "npm not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "npm not found")
- }
-
- })
-}
-
-func TestDeno_ResolveNpmBuildCommandUgly(t *testing.T) {
- t.Run("trimmed empty command falls through to the default resolver", func(t *testing.T) {
- t.Setenv("NPM_BUILD", " ")
-
- spec := requireDenoCommandSpec(t, resolveNpmBuildCommand(&build.Config{}, func(...string) core.Result {
- return core.Ok("npm")
- }))
- if !stdlibAssertEqual("npm", spec.command) {
- t.Fatalf("want %v, got %v", "npm", spec.command)
- }
- if !stdlibAssertEqual([]string{"run", "build"}, spec.args) {
- t.Fatalf("want %v, got %v", []string{"run", "build"}, spec.args)
- }
-
- })
-}
-
-func TestDeno_SplitCommandLineGood(t *testing.T) {
- t.Run("handles quoted arguments and escaped spaces", func(t *testing.T) {
- args := requireDenoArgs(t, splitCommandLine(`deno task "build docs" --flag value\ with\ spaces`))
- if !stdlibAssertEqual([]string{"deno", "task", "build docs", "--flag", "value with spaces"}, args) {
- t.Fatalf("want %v, got %v", []string{"deno", "task", "build docs", "--flag", "value with spaces"}, args)
- }
-
- })
-}
-
-func TestDeno_SplitCommandLineBad(t *testing.T) {
- t.Run("rejects unterminated quotes", func(t *testing.T) {
- result := splitCommandLine(`deno task "build docs`)
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "unterminated quote") {
- t.Fatalf("expected %v to contain %v", result.Error(), "unterminated quote")
- }
-
- })
-}
-
-func TestDeno_SplitCommandLineUgly(t *testing.T) {
- t.Run("empty input returns no args", func(t *testing.T) {
- args := requireDenoArgs(t, splitCommandLine(" "))
- if !stdlibAssertNil(args) {
- t.Fatalf("expected nil, got %v", args)
- }
-
- })
-}
diff --git a/pkg/build/builders/docker.go b/pkg/build/builders/docker.go
deleted file mode 100644
index c0877ed..0000000
--- a/pkg/build/builders/docker.go
+++ /dev/null
@@ -1,235 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// DockerBuilder builds Docker images.
-//
-// b := builders.NewDockerBuilder()
-type DockerBuilder struct{}
-
-// NewDockerBuilder creates a new Docker builder.
-//
-// b := builders.NewDockerBuilder()
-func NewDockerBuilder() *DockerBuilder {
- return &DockerBuilder{}
-}
-
-// Name returns the builder's identifier.
-//
-// name := b.Name() // → "docker"
-func (b *DockerBuilder) Name() string {
- return "docker"
-}
-
-// Detect checks if a Dockerfile or Containerfile exists in the directory.
-//
-// ok, err := b.Detect(storage.Local, ".")
-func (b *DockerBuilder) Detect(fs storage.Medium, dir string) core.Result {
- if build.ResolveDockerfilePath(fs, dir) != "" {
- return core.Ok(true)
- }
- return core.Ok(false)
-}
-
-// Build builds Docker images for the specified targets.
-//
-// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
-func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("DockerBuilder.Build", "config is nil", nil))
- }
- filesystem := ensureBuildFilesystem(cfg)
-
- dockerCommandResult := b.resolveDockerCli()
- if !dockerCommandResult.OK {
- return dockerCommandResult
- }
- dockerCommand := dockerCommandResult.Value.(string)
-
- // Ensure buildx is available
- ensured := b.ensureBuildx(ctx, dockerCommand)
- if !ensured.OK {
- return ensured
- }
-
- // Determine Docker manifest path
- dockerfile := cfg.Dockerfile
- if dockerfile == "" {
- dockerfile = build.ResolveDockerfilePath(filesystem, cfg.ProjectDir)
- } else if !ax.IsAbs(dockerfile) {
- dockerfile = ax.Join(cfg.ProjectDir, dockerfile)
- }
-
- // Validate Dockerfile exists
- if dockerfile == "" || !filesystem.IsFile(dockerfile) {
- return core.Fail(core.E("DockerBuilder.Build", "Dockerfile or Containerfile not found", nil))
- }
-
- // Determine image name
- imageName := cfg.Image
- if imageName == "" {
- imageName = cfg.Name
- }
- if imageName == "" {
- imageName = ax.Base(cfg.ProjectDir)
- }
-
- // Build platform string from targets
- buildTargets := targets
- if len(buildTargets) == 0 {
- buildTargets = []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
- }
-
- var platforms []string
- for _, t := range buildTargets {
- platforms = append(platforms, core.Sprintf("%s/%s", t.OS, t.Arch))
- }
-
- // Determine registry
- registry := cfg.Registry
- if registry == "" {
- registry = "ghcr.io"
- }
-
- // Determine tags
- tags := cfg.Tags
- if len(tags) == 0 {
- tags = []string{"latest"}
- if cfg.Version != "" {
- tags = append(tags, cfg.Version)
- }
- }
-
- // Build full image references
- var imageRefs []string
- for _, tag := range tags {
- expandedTag := build.ExpandVersionTemplate(tag, cfg.Version)
-
- if registry != "" {
- imageRefs = append(imageRefs, core.Sprintf("%s/%s:%s", registry, imageName, expandedTag))
- } else {
- imageRefs = append(imageRefs, core.Sprintf("%s:%s", imageName, expandedTag))
- }
- }
-
- // Build the docker buildx command
- args := []string{"buildx", "build"}
-
- // Multi-platform support
- args = append(args, "--platform", core.Join(",", platforms...))
-
- // Add all tags
- for _, ref := range imageRefs {
- args = append(args, "-t", ref)
- }
-
- // Dockerfile path
- args = append(args, "-f", dockerfile)
-
- // Build arguments
- for k, v := range cfg.BuildArgs {
- expandedValue := build.ExpandVersionTemplate(v, cfg.Version)
- args = append(args, "--build-arg", core.Sprintf("%s=%s", k, expandedValue))
- }
-
- // Always add VERSION build arg if version is set
- if cfg.Version != "" {
- args = append(args, "--build-arg", core.Sprintf("VERSION=%s", cfg.Version))
- }
-
- safeImageName := core.Replace(imageName, "/", "_")
-
- // Output to local docker images or push.
- // `--load` only works for a single target, so multi-platform local builds
- // fall back to an OCI archive on disk.
- useLoad := cfg.Load && !cfg.Push && len(buildTargets) == 1
- if cfg.Push {
- args = append(args, "--push")
- } else if useLoad {
- args = append(args, "--load")
- } else {
- // Local Docker builds emit an OCI archive so the build output is a file.
- outputPath := ax.Join(cfg.OutputDir, core.Sprintf("%s.tar", safeImageName))
- args = append(args, "--output", core.Sprintf("type=oci,dest=%s", outputPath))
- }
-
- // Build context (project directory)
- args = append(args, cfg.ProjectDir)
-
- // Create output directory
- created := filesystem.EnsureDir(cfg.OutputDir)
- if !created.OK {
- return core.Fail(core.E("DockerBuilder.Build", "failed to create output directory", core.NewError(created.Error())))
- }
-
- core.Print(nil, "Building Docker image: %s", imageName)
- core.Print(nil, " Platforms: %s", core.Join(", ", platforms...))
- core.Print(nil, " Tags: %s", core.Join(", ", imageRefs...))
-
- // Build once for the full platform set. Docker buildx produces a single
- // multi-arch image or OCI archive from the combined platform list.
- executed := ax.ExecWithEnv(ctx, cfg.ProjectDir, build.BuildEnvironment(cfg), dockerCommand, args...)
- if !executed.OK {
- return core.Fail(core.E("DockerBuilder.Build", "buildx build failed", core.NewError(executed.Error())))
- }
-
- artifactPath := imageRefs[0]
- if !cfg.Push && !useLoad {
- artifactPath = ax.Join(cfg.OutputDir, core.Sprintf("%s.tar", safeImageName))
- }
-
- primaryTarget := buildTargets[0]
- return core.Ok([]build.Artifact{{
- Path: artifactPath,
- OS: primaryTarget.OS,
- Arch: primaryTarget.Arch,
- }})
-}
-
-// resolveDockerCli returns the executable path for the docker CLI.
-func (b *DockerBuilder) resolveDockerCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/docker",
- "/opt/homebrew/bin/docker",
- "/Applications/Docker.app/Contents/Resources/bin/docker",
- }
- }
-
- command := ax.ResolveCommand("docker", paths...)
- if !command.OK {
- return core.Fail(core.E("DockerBuilder.resolveDockerCli", "docker CLI not found. Install it from https://docs.docker.com/get-docker/", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// ensureBuildx ensures docker buildx is available and has a builder.
-func (b *DockerBuilder) ensureBuildx(ctx context.Context, dockerCommand string) core.Result {
- // Check if buildx is available
- version := ax.Exec(ctx, dockerCommand, "buildx", "version")
- if !version.OK {
- return core.Fail(core.E("DockerBuilder.ensureBuildx", "buildx is not available. Install it from https://docs.docker.com/buildx/working-with-buildx/", core.NewError(version.Error())))
- }
-
- // Check if we have a builder, create one if not
- inspected := ax.Exec(ctx, dockerCommand, "buildx", "inspect", "--bootstrap")
- if !inspected.OK {
- // Try to create a builder
- created := ax.Exec(ctx, dockerCommand, "buildx", "create", "--use", "--bootstrap")
- if !created.OK {
- return core.Fail(core.E("DockerBuilder.ensureBuildx", "failed to create buildx builder", core.NewError(created.Error())))
- }
- }
-
- return core.Ok(nil)
-}
diff --git a/pkg/build/builders/docker_example_test.go b/pkg/build/builders/docker_example_test.go
deleted file mode 100644
index 59353d4..0000000
--- a/pkg/build/builders/docker_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewDockerBuilder references NewDockerBuilder on this package API surface.
-func ExampleNewDockerBuilder() {
- _ = NewDockerBuilder
- core.Println("NewDockerBuilder")
- // Output: NewDockerBuilder
-}
-
-// ExampleDockerBuilder_Name references DockerBuilder.Name on this package API surface.
-func ExampleDockerBuilder_Name() {
- _ = (*DockerBuilder).Name
- core.Println("DockerBuilder.Name")
- // Output: DockerBuilder.Name
-}
-
-// ExampleDockerBuilder_Detect references DockerBuilder.Detect on this package API surface.
-func ExampleDockerBuilder_Detect() {
- _ = (*DockerBuilder).Detect
- core.Println("DockerBuilder.Detect")
- // Output: DockerBuilder.Detect
-}
-
-// ExampleDockerBuilder_Build references DockerBuilder.Build on this package API surface.
-func ExampleDockerBuilder_Build() {
- _ = (*DockerBuilder).Build
- core.Println("DockerBuilder.Build")
- // Output: DockerBuilder.Build
-}
diff --git a/pkg/build/builders/docker_test.go b/pkg/build/builders/docker_test.go
deleted file mode 100644
index 8c3e148..0000000
--- a/pkg/build/builders/docker_test.go
+++ /dev/null
@@ -1,549 +0,0 @@
-package builders
-
-import (
- "context"
- "runtime"
- "testing"
-
- "dappco.re/go/build/internal/ax"
-
- core "dappco.re/go"
- "dappco.re/go/build/pkg/build"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-func setupFakeDockerToolchain(t *testing.T, binDir string) {
- t.Helper()
-
- script := `#!/bin/sh
-set -eu
-
- log_file="${DOCKER_BUILD_LOG_FILE:-}"
- if [ -n "$log_file" ]; then
- printf '%s\n' "$*" >> "$log_file"
- env | sort >> "$log_file"
- fi
-
- if [ "${1:-}" = "buildx" ] && [ "${2:-}" = "build" ]; then
- dest=""
- while [ $# -gt 0 ]; do
- if [ "$1" = "--output" ]; then
- shift
- dest="$(printf '%s' "$1" | sed -n 's#type=oci,dest=##p')"
- fi
- shift
- done
- if [ -n "$dest" ]; then
- mkdir -p "$(dirname "$dest")"
- printf 'oci archive\n' > "$dest"
- fi
-fi
-`
- if result := ax.WriteFile(ax.Join(binDir, "docker"), []byte(script), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func TestDocker_DockerBuilderNameGood(t *testing.T) {
- builder := NewDockerBuilder()
- if !stdlibAssertEqual("docker", builder.Name()) {
- t.Fatalf("want %v, got %v", "docker", builder.Name())
- }
-
-}
-
-func TestDocker_DockerBuilderDetectGood(t *testing.T) {
- fs := coreio.Local
-
- t.Run("detects Dockerfile", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "Dockerfile"), []byte("FROM alpine\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewDockerBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects Containerfile", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "Containerfile"), []byte("FROM alpine\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewDockerBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for empty directory", func(t *testing.T) {
- dir := t.TempDir()
-
- builder := NewDockerBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false for non-Docker project", func(t *testing.T) {
- dir := t.TempDir()
- // Create a Go project instead
- if result := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewDockerBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("does not match docker-compose.yml", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "docker-compose.yml"), []byte("version: '3'\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewDockerBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("does not match Dockerfile in subdirectory", func(t *testing.T) {
- dir := t.TempDir()
- subDir := ax.Join(dir, "subdir")
- if result := ax.MkdirAll(subDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(subDir, "Dockerfile"), []byte("FROM alpine\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewDockerBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDocker_DockerBuilderInterfaceGood(t *testing.T) {
- builder := NewDockerBuilder()
- var _ build.Builder = builder
- if !stdlibAssertEqual("docker", builder.Name()) {
- t.Fatalf("want %v, got %v", "docker", builder.Name())
- }
- detected := requireCPPBool(t, builder.Detect(nil, t.TempDir()))
- if detected {
- t.Fatal("expected empty temp directory not to be detected")
- }
-}
-
-func TestDocker_DockerBuilderResolveDockerCliGood(t *testing.T) {
- builder := NewDockerBuilder()
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "docker")
- if result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- command := requireCPPString(t, builder.resolveDockerCli(fallbackPath))
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestDocker_DockerBuilderResolveDockerCliBad(t *testing.T) {
- builder := NewDockerBuilder()
- t.Setenv("PATH", "")
-
- result := builder.resolveDockerCli(ax.Join(t.TempDir(), "missing-docker"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "docker CLI not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "docker CLI not found")
- }
-
-}
-
-func TestDocker_DockerBuilderBuildGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeDockerToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- if result := ax.WriteFile(ax.Join(projectDir, "Containerfile"), []byte("FROM alpine:latest\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logDir := t.TempDir()
- logPath := ax.Join(logDir, "docker.log")
- t.Setenv("DOCKER_BUILD_LOG_FILE", logPath)
-
- builder := NewDockerBuilder()
- cfg := &build.Config{
- FS: coreio.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "sample-app",
- Image: "owner/repo",
- Env: []string{"FOO=bar"},
- }
- targets := []build.Target{
- {OS: "linux", Arch: "amd64"},
- {OS: "linux", Arch: "arm64"},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- expectedPath := ax.Join(outputDir, "owner_repo.tar")
- if !stdlibAssertEqual(expectedPath, artifacts[0].Path) {
- t.Fatalf("want %v, got %v", expectedPath, artifacts[0].Path)
- }
- if !stdlibAssertEqual("linux", artifacts[0].OS) {
- t.Fatalf("want %v, got %v", "linux", artifacts[0].OS)
- }
- if !stdlibAssertEqual("amd64", artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", "amd64", artifacts[0].Arch)
- }
- if result := ax.Stat(expectedPath); !result.OK {
- t.Fatalf("expected file to exist: %v", expectedPath)
- }
-
- logContent := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- log := string(logContent)
- buildxCount := len(core.Split(log, "buildx build")) - 1
- if !stdlibAssertEqual(1, buildxCount) {
- t.Fatalf("want %v, got %v", 1, buildxCount)
- }
- if !stdlibAssertContains(log, "--platform") {
- t.Fatalf("expected %v to contain %v", log, "--platform")
- }
- if !stdlibAssertContains(log, "linux/amd64,linux/arm64") {
- t.Fatalf("expected %v to contain %v", log, "linux/amd64,linux/arm64")
- }
- if !stdlibAssertContains(log, "--output") {
- t.Fatalf("expected %v to contain %v", log, "--output")
- }
- if !stdlibAssertContains(log, "type=oci,dest="+expectedPath) {
- t.Fatalf("expected %v to contain %v", log, "type=oci,dest="+expectedPath)
- }
- if !stdlibAssertContains(log, "FOO=bar") {
- t.Fatalf("expected %v to contain %v", log, "FOO=bar")
- }
-
- artifacts = requireCPPArtifacts(t, builder.Build(context.Background(), cfg, nil))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(runtime.GOOS, artifacts[0].OS) {
- t.Fatalf("want %v, got %v", runtime.GOOS, artifacts[0].OS)
- }
- if !stdlibAssertEqual(runtime.GOARCH, artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", runtime.GOARCH, artifacts[0].Arch)
- }
-
-}
-
-func TestDocker_DockerBuilderBuild_ResolvesRelativeDockerfileGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeDockerToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- dockerfilePath := ax.Join(projectDir, "dockerfiles", "Dockerfile.app")
- if result := ax.MkdirAll(ax.Dir(dockerfilePath), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result := ax.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logDir := t.TempDir()
- logPath := ax.Join(logDir, "docker.log")
- t.Setenv("DOCKER_BUILD_LOG_FILE", logPath)
-
- builder := NewDockerBuilder()
- cfg := &build.Config{
- FS: coreio.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Dockerfile: "dockerfiles/Dockerfile.app",
- Image: "owner/repo",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(ax.Join(outputDir, "owner_repo.tar")); !result.OK {
- t.Fatalf("expected file to exist: %v", ax.Join(outputDir, "owner_repo.tar"))
- }
-
- logContent := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- log := string(logContent)
- if !stdlibAssertContains(log, "-f") {
- t.Fatalf("expected %v to contain %v", log, "-f")
- }
- if !stdlibAssertContains(log, dockerfilePath) {
- t.Fatalf("expected %v to contain %v", log, dockerfilePath)
- }
-
-}
-
-func TestDocker_DockerBuilderBuild_Containerfile_Good(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeDockerToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- if result := ax.WriteFile(ax.Join(projectDir, "Containerfile"), []byte("FROM alpine:latest\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- builder := NewDockerBuilder()
- cfg := &build.Config{
- FS: coreio.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Image: "owner/repo",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(ax.Join(outputDir, "owner_repo.tar")); !result.OK {
- t.Fatalf("expected file to exist: %v", ax.Join(outputDir, "owner_repo.tar"))
- }
-
-}
-
-func TestDocker_DockerBuilderBuild_Load_Good(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeDockerToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- if result := ax.WriteFile(ax.Join(projectDir, "Dockerfile"), []byte("FROM alpine:latest\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logDir := t.TempDir()
- logPath := ax.Join(logDir, "docker.log")
- t.Setenv("DOCKER_BUILD_LOG_FILE", logPath)
-
- builder := NewDockerBuilder()
- cfg := &build.Config{
- FS: coreio.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Image: "owner/repo",
- Load: true,
- Env: []string{"FOO=bar"},
- }
- targets := []build.Target{
- {OS: "linux", Arch: "amd64"},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual("ghcr.io/owner/repo:latest", artifacts[0].Path) {
- t.Fatalf("want %v, got %v", "ghcr.io/owner/repo:latest", artifacts[0].Path)
- }
- if !stdlibAssertEqual("linux", artifacts[0].OS) {
- t.Fatalf("want %v, got %v", "linux", artifacts[0].OS)
- }
- if !stdlibAssertEqual("amd64", artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", "amd64", artifacts[0].Arch)
- }
- if !coreio.Local.IsDir(outputDir) {
- t.Fatalf("expected directory to exist: %v", outputDir)
- }
-
- logContent := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- log := string(logContent)
- if !stdlibAssertContains(log, "buildx build") {
- t.Fatalf("expected %v to contain %v", log, "buildx build")
- }
- if !stdlibAssertContains(log, "--load") {
- t.Fatalf("expected %v to contain %v", log, "--load")
- }
- if stdlibAssertContains(log, "--output") {
- t.Fatalf("expected %v not to contain %v", log, "--output")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestDocker_NewDockerBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewDockerBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDocker_NewDockerBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewDockerBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDocker_NewDockerBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewDockerBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDocker_DockerBuilder_Name_Good(t *core.T) {
- subject := &DockerBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDocker_DockerBuilder_Name_Bad(t *core.T) {
- subject := &DockerBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDocker_DockerBuilder_Name_Ugly(t *core.T) {
- subject := &DockerBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDocker_DockerBuilder_Detect_Good(t *core.T) {
- subject := &DockerBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(coreio.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDocker_DockerBuilder_Detect_Bad(t *core.T) {
- subject := &DockerBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(coreio.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDocker_DockerBuilder_Detect_Ugly(t *core.T) {
- subject := &DockerBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(coreio.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDocker_DockerBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &DockerBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDocker_DockerBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &DockerBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDocker_DockerBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &DockerBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/docs.go b/pkg/build/builders/docs.go
deleted file mode 100644
index 188dd67..0000000
--- a/pkg/build/builders/docs.go
+++ /dev/null
@@ -1,148 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// DocsBuilder builds MkDocs projects.
-//
-// b := builders.NewDocsBuilder()
-type DocsBuilder struct{}
-
-// NewDocsBuilder creates a new DocsBuilder instance.
-//
-// b := builders.NewDocsBuilder()
-func NewDocsBuilder() *DocsBuilder {
- return &DocsBuilder{}
-}
-
-// Name returns the builder's identifier.
-//
-// name := b.Name() // → "docs"
-func (b *DocsBuilder) Name() string {
- return "docs"
-}
-
-// Detect checks if this builder can handle the project in the given directory.
-//
-// ok, err := b.Detect(storage.Local, ".")
-func (b *DocsBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(build.IsDocsProject(fs, dir))
-}
-
-// Build runs mkdocs build and packages the generated site into a zip archive.
-//
-// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
-func (b *DocsBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("DocsBuilder.Build", "config is nil", nil))
- }
- filesystem := ensureBuildFilesystem(cfg)
-
- targets = defaultRuntimeTargets(targets, runtime.GOOS, runtime.GOARCH)
-
- outputDir := cfg.OutputDir
- if outputDir == "" {
- outputDir = defaultOutputDir(cfg)
- }
- created := ensureOutputDir(filesystem, outputDir, "DocsBuilder.Build")
- if !created.OK {
- return created
- }
-
- configPath := b.resolveMkDocsConfigPath(cfg.FS, cfg.ProjectDir)
- if configPath == "" {
- return core.Fail(core.E("DocsBuilder.Build", "mkdocs.yml or mkdocs.yaml not found", nil))
- }
-
- mkdocsCommandResult := b.resolveMkDocsCli()
- if !mkdocsCommandResult.OK {
- return mkdocsCommandResult
- }
- mkdocsCommand := mkdocsCommandResult.Value.(string)
-
- var artifacts []build.Artifact
- for _, target := range targets {
- platformDirResult := ensurePlatformDir(filesystem, outputDir, target, "DocsBuilder.Build")
- if !platformDirResult.OK {
- return platformDirResult
- }
- platformDir := platformDirResult.Value.(string)
-
- siteDir := ax.Join(platformDir, "site")
- createdSite := filesystem.EnsureDir(siteDir)
- if !createdSite.OK {
- return core.Fail(core.E("DocsBuilder.Build", "failed to create site directory", core.NewError(createdSite.Error())))
- }
-
- env := configuredTargetEnv(cfg, target, standardTargetValues(outputDir, platformDir, target)...)
-
- args := []string{"build", "--clean", "--site-dir", siteDir, "--config-file", configPath}
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, env, mkdocsCommand, args...)
- if !output.OK {
- return core.Fail(core.E("DocsBuilder.Build", "mkdocs build failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- bundlePath := ax.Join(platformDir, b.bundleName(cfg)+".zip")
- bundled := b.bundleSite(filesystem, siteDir, bundlePath)
- if !bundled.OK {
- return bundled
- }
-
- artifacts = append(artifacts, build.Artifact{
- Path: bundlePath,
- OS: target.OS,
- Arch: target.Arch,
- })
- }
-
- return core.Ok(artifacts)
-}
-
-// resolveMkDocsConfigPath returns the MkDocs config file path if present.
-func (b *DocsBuilder) resolveMkDocsConfigPath(fs storage.Medium, projectDir string) string {
- return build.ResolveMkDocsConfigPath(fs, projectDir)
-}
-
-// resolveMkDocsCli returns the executable path for the mkdocs CLI.
-func (b *DocsBuilder) resolveMkDocsCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/mkdocs",
- "/opt/homebrew/bin/mkdocs",
- }
- }
-
- command := ax.ResolveCommand("mkdocs", paths...)
- if !command.OK {
- return core.Fail(core.E("DocsBuilder.resolveMkDocsCli", "mkdocs CLI not found. Install it with: pip install mkdocs", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// bundleName returns the bundle filename stem.
-func (b *DocsBuilder) bundleName(cfg *build.Config) string {
- if cfg.Name != "" {
- return cfg.Name
- }
- if cfg.ProjectDir != "" {
- return ax.Base(cfg.ProjectDir)
- }
- return "docs-site"
-}
-
-// bundleSite creates a zip bundle containing the generated MkDocs site.
-func (b *DocsBuilder) bundleSite(fs storage.Medium, siteDir, bundlePath string) core.Result {
- return bundleZipTree(fs, siteDir, bundlePath, "DocsBuilder.bundleSite", nil)
-}
-
-// Ensure DocsBuilder implements the Builder interface.
-var _ build.Builder = (*DocsBuilder)(nil)
diff --git a/pkg/build/builders/docs_example_test.go b/pkg/build/builders/docs_example_test.go
deleted file mode 100644
index 7890b5c..0000000
--- a/pkg/build/builders/docs_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewDocsBuilder references NewDocsBuilder on this package API surface.
-func ExampleNewDocsBuilder() {
- _ = NewDocsBuilder
- core.Println("NewDocsBuilder")
- // Output: NewDocsBuilder
-}
-
-// ExampleDocsBuilder_Name references DocsBuilder.Name on this package API surface.
-func ExampleDocsBuilder_Name() {
- _ = (*DocsBuilder).Name
- core.Println("DocsBuilder.Name")
- // Output: DocsBuilder.Name
-}
-
-// ExampleDocsBuilder_Detect references DocsBuilder.Detect on this package API surface.
-func ExampleDocsBuilder_Detect() {
- _ = (*DocsBuilder).Detect
- core.Println("DocsBuilder.Detect")
- // Output: DocsBuilder.Detect
-}
-
-// ExampleDocsBuilder_Build references DocsBuilder.Build on this package API surface.
-func ExampleDocsBuilder_Build() {
- _ = (*DocsBuilder).Build
- core.Println("DocsBuilder.Build")
- // Output: DocsBuilder.Build
-}
diff --git a/pkg/build/builders/docs_test.go b/pkg/build/builders/docs_test.go
deleted file mode 100644
index 1a95eb7..0000000
--- a/pkg/build/builders/docs_test.go
+++ /dev/null
@@ -1,364 +0,0 @@
-package builders
-
-import (
- "archive/zip"
- "context"
- stdio "io"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func TestDocs_DocsBuilderNameGood(t *testing.T) {
- builder := NewDocsBuilder()
- if !stdlibAssertEqual("docs", builder.Name()) {
- t.Fatalf("want %v, got %v", "docs", builder.Name())
- }
-
-}
-
-func TestDocs_DocsBuilderDetectGood(t *testing.T) {
- fs := storage.Local
-
- t.Run("detects mkdocs.yml", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "mkdocs.yml"), []byte("site_name: Demo\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewDocsBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects mkdocs.yaml", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "mkdocs.yaml"), []byte("site_name: Demo\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewDocsBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false without mkdocs.yml", func(t *testing.T) {
- builder := NewDocsBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, t.TempDir()))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDocs_DocsBuilderBuildGood(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("mkdocs test fixture uses a shell script")
- }
-
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "mkdocs.yaml"), []byte("site_name: Demo\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- binDir := t.TempDir()
- mkdocsPath := ax.Join(binDir, "mkdocs")
- script := "#!/bin/sh\nset -eu\nif [ -n \"${DOCS_BUILD_LOG_FILE:-}\" ]; then\n env | sort > \"${DOCS_BUILD_LOG_FILE}\"\nfi\nsite_dir=\"\"\nwhile [ $# -gt 0 ]; do\n if [ \"$1\" = \"--site-dir\" ]; then\n shift\n site_dir=\"$1\"\n fi\n shift\ndone\nmkdir -p \"$site_dir\"\nprintf '%s' 'demo docs' > \"$site_dir/index.html\"\n"
- if result := ax.WriteFile(mkdocsPath, []byte(script), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
- logPath := ax.Join(t.TempDir(), "docs.env")
-
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: dir,
- OutputDir: ax.Join(dir, "dist"),
- Name: "demo-site",
- Env: []string{"FOO=bar", "DOCS_BUILD_LOG_FILE=" + logPath},
- }
-
- builder := NewDocsBuilder()
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- artifact := artifacts[0]
- if !stdlibAssertEqual("linux", artifact.OS) {
- t.Fatalf("want %v, got %v", "linux", artifact.OS)
- }
- if !stdlibAssertEqual("amd64", artifact.Arch) {
- t.Fatalf("want %v, got %v", "amd64", artifact.Arch)
- }
- if result := ax.Stat(artifact.Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifact.Path)
- }
-
- reader, err := zip.OpenReader(artifact.Path)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
-
- defer func() { _ = reader.Close() }()
- if len(reader.File) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(reader.File))
- }
- if !stdlibAssertEqual("index.html", reader.File[0].Name) {
- t.Fatalf("want %v, got %v", "index.html", reader.File[0].Name)
- }
-
- file, err := reader.File[0].Open()
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
-
- defer func() { _ = file.Close() }()
-
- data, err := stdio.ReadAll(file)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if !stdlibAssertEqual("demo docs", string(data)) {
- t.Fatalf("want %v, got %v", "demo docs", string(data))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "FOO=bar") {
- t.Fatalf("expected %v to contain %v", string(content), "FOO=bar")
- }
- if !stdlibAssertContains(string(content), "GOOS=linux") {
- t.Fatalf("expected %v to contain %v", string(content), "GOOS=linux")
- }
- if !stdlibAssertContains(string(content), "GOARCH=amd64") {
- t.Fatalf("expected %v to contain %v", string(content), "GOARCH=amd64")
- }
- if !stdlibAssertContains(string(content), "TARGET_OS=linux") {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_OS=linux")
- }
- if !stdlibAssertContains(string(content), "TARGET_ARCH=amd64") {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_ARCH=amd64")
- }
- if !stdlibAssertContains(string(content), "OUTPUT_DIR="+ax.Join(dir, "dist")) {
- t.Fatalf("expected %v to contain %v", string(content), "OUTPUT_DIR="+ax.Join(dir, "dist"))
- }
- if !stdlibAssertContains(string(content), "TARGET_DIR="+ax.Join(dir, "dist", "linux_amd64")) {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_DIR="+ax.Join(dir, "dist", "linux_amd64"))
- }
- if !stdlibAssertContains(string(content), "NAME=demo-site") {
- t.Fatalf("expected %v to contain %v", string(content), "NAME=demo-site")
- }
-
-}
-
-func TestDocs_DocsBuilderBuild_Good_NestedConfig(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("mkdocs test fixture uses a shell script")
- }
-
- dir := t.TempDir()
- if result := ax.MkdirAll(ax.Join(dir, "docs"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result := ax.WriteFile(ax.Join(dir, "docs", "mkdocs.yaml"), []byte("site_name: Demo\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- binDir := t.TempDir()
- mkdocsPath := ax.Join(binDir, "mkdocs")
- script := "#!/bin/sh\nset -eu\nif [ -n \"${DOCS_BUILD_LOG_FILE:-}\" ]; then\n env | sort >> \"${DOCS_BUILD_LOG_FILE}\"\n printf '%s\\n' \"$@\" >> \"${DOCS_BUILD_LOG_FILE}\"\nfi\nsite_dir=\"\"\nwhile [ $# -gt 0 ]; do\n if [ \"$1\" = \"--site-dir\" ]; then\n shift\n site_dir=\"$1\"\n fi\n shift\ndone\nmkdir -p \"$site_dir\"\nprintf '%s' 'demo docs' > \"$site_dir/index.html\"\n"
- if result := ax.WriteFile(mkdocsPath, []byte(script), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
- logPath := ax.Join(t.TempDir(), "docs.args")
-
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: dir,
- OutputDir: ax.Join(dir, "dist"),
- Name: "demo-site",
- Env: []string{"DOCS_BUILD_LOG_FILE=" + logPath},
- }
-
- builder := NewDocsBuilder()
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "--config-file") {
- t.Fatalf("expected %v to contain %v", string(content), "--config-file")
- }
- if !stdlibAssertContains(string(content), "docs/mkdocs.yaml") {
- t.Fatalf("expected %v to contain %v", string(content), "docs/mkdocs.yaml")
- }
- if !stdlibAssertContains(string(content), "TARGET_DIR="+ax.Join(dir, "dist", "linux_amd64")) {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_DIR="+ax.Join(dir, "dist", "linux_amd64"))
- }
-
-}
-
-func TestDocs_DocsBuilderBuildBad(t *testing.T) {
- builder := NewDocsBuilder()
-
- t.Run("returns error when config is nil", func(t *testing.T) {
- result := builder.Build(context.Background(), nil, []build.Target{{OS: "linux", Arch: "amd64"}})
- if result.OK {
- t.Fatal("expected error")
- }
-
- })
-
- t.Run("returns error when mkdocs.yml is missing", func(t *testing.T) {
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: t.TempDir(),
- OutputDir: t.TempDir(),
- }
-
- result := builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
- if result.OK {
- t.Fatal("expected error")
- }
-
- })
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestDocs_NewDocsBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewDocsBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDocs_NewDocsBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewDocsBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDocs_NewDocsBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewDocsBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDocs_DocsBuilder_Name_Good(t *core.T) {
- subject := &DocsBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDocs_DocsBuilder_Name_Bad(t *core.T) {
- subject := &DocsBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDocs_DocsBuilder_Name_Ugly(t *core.T) {
- subject := &DocsBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDocs_DocsBuilder_Detect_Good(t *core.T) {
- subject := &DocsBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDocs_DocsBuilder_Detect_Bad(t *core.T) {
- subject := &DocsBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDocs_DocsBuilder_Detect_Ugly(t *core.T) {
- subject := &DocsBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDocs_DocsBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &DocsBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDocs_DocsBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &DocsBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDocs_DocsBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &DocsBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/env.go b/pkg/build/builders/env.go
deleted file mode 100644
index 34d0ddc..0000000
--- a/pkg/build/builders/env.go
+++ /dev/null
@@ -1,273 +0,0 @@
-package builders
-
-import (
- "archive/zip"
- stdio "io"
- stdfs "io/fs"
- "runtime"
- "slices"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// appendConfiguredEnv returns a fresh environment slice that includes the
-// configured build environment, derived cache variables, and any
-// builder-specific values.
-func appendConfiguredEnv(cfg *build.Config, extra ...string) []string {
- return build.BuildEnvironment(cfg, extra...)
-}
-
-// ensureBuildFilesystem returns the filesystem associated with cfg, falling
-// back to storage.Local for zero-value configs. When cfg is non-nil, the fallback is
-// also written back so downstream helpers that read cfg.FS stay safe.
-func ensureBuildFilesystem(cfg *build.Config) storage.Medium {
- if cfg == nil {
- return storage.Local
- }
- if cfg.FS == nil {
- cfg.FS = storage.Local
- }
- return cfg.FS
-}
-
-func defaultHostTargets(targets []build.Target) []build.Target {
- if len(targets) > 0 {
- return targets
- }
- goos := core.Env("GOOS")
- if goos == "" {
- goos = runtime.GOOS
- }
- goarch := core.Env("GOARCH")
- if goarch == "" {
- goarch = runtime.GOARCH
- }
- return []build.Target{{OS: goos, Arch: goarch}}
-}
-
-func defaultRuntimeTargets(targets []build.Target, osName, archName string) []build.Target {
- if len(targets) > 0 {
- return targets
- }
- return []build.Target{{OS: osName, Arch: archName}}
-}
-
-func defaultLinuxTargets(targets []build.Target) []build.Target {
- if len(targets) > 0 {
- return targets
- }
- return []build.Target{{OS: "linux", Arch: "amd64"}}
-}
-
-func defaultOutputDir(cfg *build.Config) string {
- if cfg == nil || cfg.OutputDir != "" {
- return ""
- }
- return ax.Join(cfg.ProjectDir, "dist")
-}
-
-func ensureOutputDir(fs storage.Medium, outputDir, operation string) core.Result {
- if outputDir == "" {
- return core.Ok(nil)
- }
- created := fs.EnsureDir(outputDir)
- if !created.OK {
- return core.Fail(core.E(operation, "failed to create output directory", core.NewError(created.Error())))
- }
- return core.Ok(nil)
-}
-
-func platformName(target build.Target) string {
- return core.Sprintf("%s_%s", target.OS, target.Arch)
-}
-
-func platformDir(outputDir string, target build.Target) string {
- name := platformName(target)
- if outputDir == "" {
- return name
- }
- return ax.Join(outputDir, name)
-}
-
-func ensurePlatformDir(fs storage.Medium, outputDir string, target build.Target, operation string) core.Result {
- dir := platformDir(outputDir, target)
- created := fs.EnsureDir(dir)
- if !created.OK {
- return core.Fail(core.E(operation, "failed to create platform directory", core.NewError(created.Error())))
- }
- return core.Ok(dir)
-}
-
-func standardTargetValues(outputDir, targetDir string, target build.Target) []string {
- return []string{
- core.Sprintf("GOOS=%s", target.OS),
- core.Sprintf("GOARCH=%s", target.Arch),
- core.Sprintf("TARGET_OS=%s", target.OS),
- core.Sprintf("TARGET_ARCH=%s", target.Arch),
- core.Sprintf("OUTPUT_DIR=%s", outputDir),
- core.Sprintf("TARGET_DIR=%s", targetDir),
- }
-}
-
-func configuredTargetEnv(cfg *build.Config, target build.Target, values ...string) []string {
- env := appendConfiguredEnv(cfg, values...)
- return appendNameVersionEnv(env, cfg)
-}
-
-func appendNameVersionEnv(env []string, cfg *build.Config) []string {
- if cfg == nil {
- return env
- }
- if cfg.Name != "" {
- env = append(env, core.Sprintf("NAME=%s", cfg.Name))
- }
- if cfg.Version != "" {
- env = append(env, core.Sprintf("VERSION=%s", cfg.Version))
- }
- return env
-}
-
-func cgoEnvValue(enabled bool) string {
- if enabled {
- return "CGO_ENABLED=1"
- }
- return "CGO_ENABLED=0"
-}
-
-type stagedOutput struct {
- outputDir string
- commandOutputDir string
- commandFS storage.Medium
- cleanup func()
-}
-
-func prepareStagedOutput(outputDir string, artifactFS storage.Medium, tempPattern, operation string) core.Result {
- stage := stagedOutput{
- outputDir: outputDir,
- commandOutputDir: outputDir,
- commandFS: artifactFS,
- cleanup: func() {},
- }
- if build.MediumIsLocal(artifactFS) {
- return core.Ok(stage)
- }
-
- stageDirResult := ax.TempDir(tempPattern)
- if !stageDirResult.OK {
- return core.Fail(core.E(operation, "failed to create local artifact staging directory", core.NewError(stageDirResult.Error())))
- }
- stageDir := stageDirResult.Value.(string)
- stage.commandOutputDir = stageDir
- stage.commandFS = storage.Local
- stage.cleanup = func() { ax.RemoveAll(stageDir) }
- return core.Ok(stage)
-}
-
-type zipExcludeFunc func(path string) bool
-
-func bundleZipTree(fs storage.Medium, rootDir, bundlePath, operation string, exclude zipExcludeFunc) core.Result {
- created := fs.EnsureDir(ax.Dir(bundlePath))
- if !created.OK {
- return core.Fail(core.E(operation, "failed to create bundle directory", core.NewError(created.Error())))
- }
-
- fileResult := fs.Create(bundlePath)
- if !fileResult.OK {
- return core.Fail(core.E(operation, "failed to create bundle file", core.NewError(fileResult.Error())))
- }
- file := fileResult.Value.(core.WriteCloser)
- defer file.Close()
-
- writer := zip.NewWriter(file)
- defer writer.Close()
-
- return writeZipTree(fs, writer, rootDir, rootDir, operation, exclude)
-}
-
-func writeZipTree(fs storage.Medium, writer *zip.Writer, rootDir, currentDir, operation string, exclude zipExcludeFunc) core.Result {
- entriesResult := fs.List(currentDir)
- if !entriesResult.OK {
- return core.Fail(core.E(operation, "failed to list directory", core.NewError(entriesResult.Error())))
- }
- entries := entriesResult.Value.([]stdfs.DirEntry)
-
- slices.SortFunc(entries, func(a, b stdfs.DirEntry) int {
- if a.Name() < b.Name() {
- return -1
- }
- if a.Name() > b.Name() {
- return 1
- }
- return 0
- })
-
- for _, entry := range entries {
- entryPath := ax.Join(currentDir, entry.Name())
- if exclude != nil && exclude(entryPath) {
- continue
- }
-
- if entry.IsDir() {
- written := writeZipTree(fs, writer, rootDir, entryPath, operation, exclude)
- if !written.OK {
- return written
- }
- continue
- }
-
- written := writeZipEntry(fs, writer, rootDir, entryPath, operation)
- if !written.OK {
- return written
- }
- }
-
- return core.Ok(nil)
-}
-
-func writeZipEntry(fs storage.Medium, writer *zip.Writer, rootDir, entryPath, operation string) core.Result {
- relPathResult := ax.Rel(rootDir, entryPath)
- if !relPathResult.OK {
- return core.Fail(core.E(operation, "failed to relativise bundle path", core.NewError(relPathResult.Error())))
- }
- relPath := relPathResult.Value.(string)
-
- infoResult := fs.Stat(entryPath)
- if !infoResult.OK {
- return core.Fail(core.E(operation, "failed to stat bundle entry", core.NewError(infoResult.Error())))
- }
- info := infoResult.Value.(stdfs.FileInfo)
-
- header, err := zip.FileInfoHeader(info)
- if err != nil {
- return core.Fail(core.E(operation, "failed to create zip header", err))
- }
- header.Name = core.Replace(relPath, ax.DS(), "/")
- header.Method = zip.Deflate
- header.SetModTime(deterministicZipTime)
-
- zipEntry, err := writer.CreateHeader(header)
- if err != nil {
- return core.Fail(core.E(operation, "failed to create zip entry", err))
- }
-
- sourceResult := fs.Open(entryPath)
- if !sourceResult.OK {
- return core.Fail(core.E(operation, "failed to open bundle entry", core.NewError(sourceResult.Error())))
- }
- source := sourceResult.Value.(core.FsFile)
-
- if _, err := stdio.Copy(zipEntry, source); err != nil {
- if closeErr := source.Close(); closeErr != nil {
- return core.Fail(core.E(operation, "failed to close bundle entry after write failure", closeErr))
- }
- return core.Fail(core.E(operation, "failed to write bundle entry", err))
- }
- if err := source.Close(); err != nil {
- return core.Fail(core.E(operation, "failed to close bundle entry", err))
- }
-
- return core.Ok(nil)
-}
diff --git a/pkg/build/builders/go.go b/pkg/build/builders/go.go
deleted file mode 100644
index 496e292..0000000
--- a/pkg/build/builders/go.go
+++ /dev/null
@@ -1,267 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// GoBuilder implements the Builder interface for Go projects.
-//
-// b := builders.NewGoBuilder()
-type GoBuilder struct{}
-
-// NewGoBuilder creates a new GoBuilder instance.
-//
-// b := builders.NewGoBuilder()
-func NewGoBuilder() *GoBuilder {
- return &GoBuilder{}
-}
-
-// Name returns the builder's identifier.
-//
-// name := b.Name() // → "go"
-func (b *GoBuilder) Name() string {
- return "go"
-}
-
-// Detect checks if this builder can handle the project in the given directory.
-// Uses IsGoProject from the build package which checks for go.mod, go.work, or wails.json.
-//
-// result := b.Detect(storage.Local, ".")
-func (b *GoBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(build.IsGoProject(fs, dir))
-}
-
-// Build compiles the Go project for the specified targets.
-// If targets is empty, it falls back to the current host platform.
-// It sets GOOS, GOARCH, and CGO_ENABLED, applies config-defined build flags
-// and ldflags, and uses garble when obfuscation is enabled.
-//
-// result := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
-func (b *GoBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("GoBuilder.Build", "config is nil", nil))
- }
- ensureBuildFilesystem(cfg)
- artifactFilesystem := build.ResolveOutputMedium(cfg)
-
- targets = defaultHostTargets(targets)
-
- outputDir := cfg.OutputDir
- if outputDir == "" && build.MediumIsLocal(artifactFilesystem) {
- outputDir = defaultOutputDir(cfg)
- }
-
- created := ensureOutputDir(artifactFilesystem, outputDir, "GoBuilder.Build")
- if !created.OK {
- return created
- }
-
- var artifacts []build.Artifact
-
- for _, target := range targets {
- artifactResult := b.buildTarget(ctx, cfg, artifactFilesystem, outputDir, target)
- if !artifactResult.OK {
- return core.Fail(core.E("GoBuilder.Build", "failed to build "+target.String(), core.NewError(artifactResult.Error())))
- }
- artifacts = append(artifacts, artifactResult.Value.(build.Artifact))
- }
-
- return core.Ok(artifacts)
-}
-
-// buildTarget compiles for a single target platform.
-func (b *GoBuilder) buildTarget(ctx context.Context, cfg *build.Config, artifactFilesystem storage.Medium, outputDir string, target build.Target) core.Result {
- // Determine output binary name
- binaryName := cfg.Name
- if binaryName == "" {
- binaryName = cfg.Project.Binary
- }
- if binaryName == "" {
- binaryName = cfg.Project.Name
- }
- if binaryName == "" {
- binaryName = ax.Base(cfg.ProjectDir)
- }
-
- // Add .exe extension for Windows
- if target.OS == "windows" && !core.HasSuffix(binaryName, ".exe") {
- binaryName += ".exe"
- }
-
- platformID := platformName(target)
- platformDirResult := ensurePlatformDir(artifactFilesystem, outputDir, target, "GoBuilder.buildTarget")
- if !platformDirResult.OK {
- return platformDirResult
- }
- platformDir := platformDirResult.Value.(string)
-
- outputPath := ax.Join(platformDir, binaryName)
- commandOutputPath := outputPath
- stageResult := prepareStagedOutput(outputDir, artifactFilesystem, "core-build-go-*", "GoBuilder.buildTarget")
- if !stageResult.OK {
- return stageResult
- }
- stage := stageResult.Value.(stagedOutput)
- defer stage.cleanup()
- if !build.MediumIsLocal(artifactFilesystem) {
- stagePlatformDir := ax.Join(stage.commandOutputDir, platformID)
- created := stage.commandFS.EnsureDir(stagePlatformDir)
- if !created.OK {
- return core.Fail(core.E("GoBuilder.buildTarget", "failed to create local platform staging directory", core.NewError(created.Error())))
- }
- commandOutputPath = ax.Join(stagePlatformDir, binaryName)
- }
-
- // Build the go/garble arguments.
- args := []string{"build"}
- if !containsString(cfg.Flags, "-trimpath") {
- args = append(args, "-trimpath")
- }
- if len(cfg.Flags) > 0 {
- args = append(args, cfg.Flags...)
- }
-
- if len(cfg.BuildTags) > 0 {
- args = append(args, "-tags", core.Join(",", cfg.BuildTags...))
- }
-
- // Add ldflags if specified, and inject the build version when needed.
- ldflags := append([]string{}, cfg.LDFlags...)
- if cfg.Version != "" && !hasVersionLDFlag(ldflags) {
- versionFlag := build.VersionLinkerFlag(cfg.Version)
- if !versionFlag.OK {
- return versionFlag
- }
- ldflags = append(ldflags, versionFlag.Value.(string))
- }
- if len(ldflags) > 0 {
- args = append(args, "-ldflags", core.Join(" ", ldflags...))
- }
-
- // Add output path
- args = append(args, "-o", commandOutputPath)
-
- // Build the configured main package path, defaulting to the project root.
- mainPackage := cfg.Project.Main
- if mainPackage == "" {
- mainPackage = "."
- }
- args = append(args, mainPackage)
-
- // Set up environment.
- env := appendConfiguredEnv(cfg, standardTargetValues(outputDir, platformDir, target)...)
- if binaryName != "" {
- env = append(env, core.Sprintf("NAME=%s", binaryName))
- }
- if cfg.Version != "" {
- env = append(env, core.Sprintf("VERSION=%s", cfg.Version))
- }
- env = append(env, cgoEnvValue(cfg.CGO))
-
- command := "go"
- if cfg.Obfuscate {
- resolved := b.resolveGarbleCli()
- if !resolved.OK {
- return resolved
- }
- command = resolved.Value.(string)
- }
-
- // Capture output for error messages
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, env, command, args...)
- if !output.OK {
- return core.Fail(core.E("GoBuilder.buildTarget", command+" build failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- if commandOutputPath != outputPath {
- copied := build.CopyMediumPath(storage.Local, commandOutputPath, artifactFilesystem, outputPath)
- if !copied.OK {
- return copied
- }
- }
-
- return core.Ok(build.Artifact{
- Path: outputPath,
- OS: target.OS,
- Arch: target.Arch,
- })
-}
-
-// resolveGarbleCli returns the executable path for the garble CLI.
-//
-// command, err := b.resolveGarbleCli()
-func (b *GoBuilder) resolveGarbleCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/garble",
- "/opt/homebrew/bin/garble",
- }
-
- paths = append(paths, garbleInstallPaths()...)
-
- if home := core.Env("HOME"); home != "" {
- paths = append(paths, ax.Join(home, "go", "bin", "garble"))
- }
- }
-
- command := ax.ResolveCommand("garble", paths...)
- if !command.OK {
- return core.Fail(core.E("GoBuilder.resolveGarbleCli", "garble CLI not found. Install it with: go install mvdan.cc/garble@latest", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// garbleInstallPaths returns the standard Go install locations for garble.
-func garbleInstallPaths() []string {
- var paths []string
-
- if gobin := core.Env("GOBIN"); gobin != "" {
- paths = append(paths, ax.Join(gobin, "garble"))
- }
-
- if gopath := core.Env("GOPATH"); gopath != "" {
- sep := ":"
- if core.Env("GOOS") == "windows" {
- sep = ";"
- }
- for _, root := range core.Split(gopath, sep) {
- root = core.Trim(root)
- if root == "" {
- continue
- }
- paths = append(paths, ax.Join(root, "bin", "garble"))
- }
- }
-
- return paths
-}
-
-// hasVersionLDFlag reports whether a version linker flag is already present.
-func hasVersionLDFlag(ldflags []string) bool {
- for _, flag := range ldflags {
- if core.Contains(flag, "main.version=") || core.Contains(flag, "main.Version=") {
- return true
- }
- }
- return false
-}
-
-// containsString reports whether a slice contains the given string.
-func containsString(values []string, needle string) bool {
- for _, value := range values {
- if value == needle {
- return true
- }
- }
- return false
-}
-
-// Ensure GoBuilder implements the Builder interface.
-var _ build.Builder = (*GoBuilder)(nil)
diff --git a/pkg/build/builders/go_example_test.go b/pkg/build/builders/go_example_test.go
deleted file mode 100644
index 4e9530e..0000000
--- a/pkg/build/builders/go_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewGoBuilder references NewGoBuilder on this package API surface.
-func ExampleNewGoBuilder() {
- _ = NewGoBuilder
- core.Println("NewGoBuilder")
- // Output: NewGoBuilder
-}
-
-// ExampleGoBuilder_Name references GoBuilder.Name on this package API surface.
-func ExampleGoBuilder_Name() {
- _ = (*GoBuilder).Name
- core.Println("GoBuilder.Name")
- // Output: GoBuilder.Name
-}
-
-// ExampleGoBuilder_Detect references GoBuilder.Detect on this package API surface.
-func ExampleGoBuilder_Detect() {
- _ = (*GoBuilder).Detect
- core.Println("GoBuilder.Detect")
- // Output: GoBuilder.Detect
-}
-
-// ExampleGoBuilder_Build references GoBuilder.Build on this package API surface.
-func ExampleGoBuilder_Build() {
- _ = (*GoBuilder).Build
- core.Println("GoBuilder.Build")
- // Output: GoBuilder.Build
-}
diff --git a/pkg/build/builders/go_test.go b/pkg/build/builders/go_test.go
deleted file mode 100644
index cb14000..0000000
--- a/pkg/build/builders/go_test.go
+++ /dev/null
@@ -1,1376 +0,0 @@
-package builders
-
-import (
- "context"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/testassert"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// setupGoTestProject creates a minimal Go project for testing.
-func setupGoTestProject(t *testing.T) string {
- t.Helper()
- dir := t.TempDir()
-
- // Create a minimal go.mod
- goMod := `module testproject
-
-go 1.21
-`
- if result := ax.WriteFile(ax.Join(dir, "go.mod"), []byte(goMod), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- // Create a minimal main.go
- mainGo := `package main
-
-func main() {
- println("hello")
-}
-`
- if result := ax.WriteFile(ax.Join(dir, "main.go"), []byte(mainGo), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return dir
-}
-
-func setupFakeBuildToolchain(t *testing.T, binDir string) {
- t.Helper()
-
- goScript := `#!/bin/sh
-set -eu
-
-log_file="${GO_BUILD_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$@" > "$log_file"
-fi
-
-env_log_file="${GO_BUILD_ENV_LOG_FILE:-}"
-if [ -n "$env_log_file" ]; then
- env | sort > "$env_log_file"
-fi
-
-if [ "${GOARCH:-}" = "invalid_arch" ]; then
- exit 1
-fi
-
-if [ -f main.go ] && grep -q "not valid go code" main.go; then
- exit 1
-fi
-
-output=""
-previous=""
-for argument in "$@"; do
- if [ "$previous" = "-o" ]; then
- output="$argument"
- break
- fi
- previous="$argument"
-done
-
-if [ -n "$output" ]; then
- mkdir -p "$(dirname "$output")"
- printf 'fake binary\n' > "$output"
- chmod +x "$output"
-fi
-`
-
- if result := ax.WriteFile(ax.Join(binDir, "go"), []byte(goScript), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- garbleScript := `#!/bin/sh
-set -eu
-
-log_file="${GARBLE_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$@" > "$log_file"
-fi
-
-exec go "$@"
-`
-
- if result := ax.WriteFile(ax.Join(binDir, "garble"), []byte(garbleScript), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func setupFakeGoBinary(t *testing.T, binDir string) {
- t.Helper()
-
- goScript := `#!/bin/sh
-set -eu
-
-log_file="${GO_BUILD_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$@" > "$log_file"
-fi
-
-env_log_file="${GO_BUILD_ENV_LOG_FILE:-}"
-if [ -n "$env_log_file" ]; then
- env | sort > "$env_log_file"
-fi
-
-if [ "${GOARCH:-}" = "invalid_arch" ]; then
- exit 1
-fi
-
-if [ -f main.go ] && grep -q "not valid go code" main.go; then
- exit 1
-fi
-
-output=""
-previous=""
-for argument in "$@"; do
- if [ "$previous" = "-o" ]; then
- output="$argument"
- break
- fi
- previous="$argument"
-done
-
-if [ -n "$output" ]; then
- mkdir -p "$(dirname "$output")"
- printf 'fake binary\n' > "$output"
- chmod +x "$output"
-fi
-`
-
- if result := ax.WriteFile(ax.Join(binDir, "go"), []byte(goScript), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func setupFakeGarbleBinary(t *testing.T, binDir string) {
- t.Helper()
-
- garbleScript := `#!/bin/sh
-set -eu
-
-log_file="${GARBLE_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$@" > "$log_file"
-fi
-
-exec go "$@"
-`
-
- if result := ax.WriteFile(ax.Join(binDir, "garble"), []byte(garbleScript), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func TestGo_GoBuilderNameGood(t *testing.T) {
- builder := NewGoBuilder()
- if !stdlibAssertEqual("go", builder.Name()) {
- t.Fatalf("want %v, got %v", "go", builder.Name())
- }
-
-}
-
-func TestGo_GoBuilderDetectGood(t *testing.T) {
- fs := storage.Local
- t.Run("detects Go project with go.mod", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewGoBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects Wails project", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewGoBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for non-Go project", func(t *testing.T) {
- dir := t.TempDir()
- // Create a Node.js project instead
- if result := ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewGoBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false for empty directory", func(t *testing.T) {
- dir := t.TempDir()
-
- builder := NewGoBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestGo_GoBuilderBuildGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeBuildToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- t.Run("builds for current platform", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testbinary",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) !=
-
- // Verify artifact properties
- 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- artifact := artifacts[0]
- if !stdlibAssertEqual(runtime.GOOS, artifact.OS) {
- t.Fatalf("want %v, got %v", runtime.GOOS, artifact.OS)
-
- // Verify binary was created
- }
- if !stdlibAssertEqual(runtime.GOARCH, artifact.Arch) {
- t.Fatalf("want %v, got %v",
-
- // Verify the path is in the expected location
- runtime.GOARCH, artifact.Arch)
- }
- if result := ax.Stat(artifact.Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifact.Path)
- }
-
- expectedName := "testbinary"
- if runtime.GOOS == "windows" {
- expectedName += ".exe"
- }
- if !stdlibAssertContains(artifact.Path, expectedName) {
- t.Fatalf("expected %v to contain %v", artifact.Path, expectedName)
- }
-
- })
-
- t.Run("defaults to current platform when targets are empty", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "fallback",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, nil))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(runtime.GOOS, artifacts[0].OS) {
- t.Fatalf("want %v, got %v", runtime.GOOS, artifacts[0].OS)
- }
- if !stdlibAssertEqual(runtime.GOARCH, artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", runtime.GOARCH, artifacts[0].Arch)
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- })
-
- t.Run("does not mutate the caller output directory when using defaults", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- Name: "mutability",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEmpty(cfg.OutputDir) {
- t.Fatalf("expected empty, got %v", cfg.OutputDir)
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist"), ax.Dir(ax.Dir(artifacts[0].Path))) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist"), ax.Dir(ax.Dir(artifacts[0].Path)))
- }
-
- })
-
- t.Run("builds multiple targets", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "multitest",
- }
- targets := []build.Target{
- {OS: "linux", Arch: "amd64"},
- {OS: "linux", Arch: "arm64"},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) !=
-
- // Verify both artifacts were created
- 2 {
- t.Fatalf("want len %v, got %v", 2, len(artifacts))
- }
-
- for i, artifact := range artifacts {
- if !stdlibAssertEqual(targets[i].OS, artifact.OS) {
- t.Fatalf("want %v, got %v", targets[i].OS, artifact.OS)
- }
- if !stdlibAssertEqual(targets[i].Arch, artifact.Arch) {
- t.Fatalf("want %v, got %v", targets[i].Arch, artifact.Arch)
- }
- if result := ax.Stat(artifact.Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifact.Path)
- }
-
- }
- })
-
- t.Run("adds .exe extension for Windows", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "wintest",
- }
- targets := []build.Target{
- {OS: "windows", Arch: "amd64"},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) !=
-
- // Verify .exe extension
- 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !(ax.Ext(artifacts[0].Path) == ".exe") {
- t.Fatal("expected true")
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- })
-
- t.Run("uses directory name when Name not specified", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "", // Empty name
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) !=
-
- // Binary should use the project directory base name
- 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- baseName := ax.Base(projectDir)
- if runtime.GOOS == "windows" {
- baseName += ".exe"
- }
- if !stdlibAssertContains(artifacts[0].Path, baseName) {
- t.Fatalf("expected %v to contain %v", artifacts[0].Path, baseName)
- }
-
- })
-
- t.Run("uses configured project binary when Name not specified", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- }
- cfg.Project.Binary = "example-binary"
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- expectedName := "example-binary"
- if runtime.GOOS == "windows" {
- expectedName += ".exe"
- }
- if !stdlibAssertContains(artifacts[0].Path, expectedName) {
- t.Fatalf("expected %v to contain %v", artifacts[0].Path, expectedName)
- }
-
- })
-
- t.Run("uses configured project name when Binary not specified", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- }
- cfg.Project.Name = "example-name"
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- expectedName := "example-name"
- if runtime.GOOS == "windows" {
- expectedName += ".exe"
- }
- if !stdlibAssertContains(artifacts[0].Path, expectedName) {
- t.Fatalf("expected %v to contain %v", artifacts[0].Path, expectedName)
- }
-
- })
-
- t.Run("applies ldflags", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "ldflagstest",
- LDFlags: []string{"-s", "-w"}, // Strip debug info
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- })
-
- t.Run("applies config flags and env", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
- logDir := t.TempDir()
- argsLogPath := ax.Join(logDir, "go-args.log")
- envLogPath := ax.Join(logDir, "go-env.log")
-
- t.Setenv("GO_BUILD_LOG_FILE", argsLogPath)
- t.Setenv("GO_BUILD_ENV_LOG_FILE", envLogPath)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "envflags",
- Version: "v1.2.3",
- Flags: []string{"-race"},
- Env: []string{"FOO=bar", "BAR=baz"},
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- argsContent := requireBuilderBytes(t, ax.ReadFile(argsLogPath))
-
- args := core.Split(core.Trim(string(argsContent)), "\n")
- if stdlibAssertEmpty(args) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertEqual("build", args[0]) {
- t.Fatalf("want %v, got %v", "build", args[0])
- }
- if !stdlibAssertContains(args, "-trimpath") {
- t.Fatalf("expected %v to contain %v", args, "-trimpath")
- }
- if !stdlibAssertContains(args, "-race") {
- t.Fatalf("expected %v to contain %v", args, "-race")
- }
-
- envContent := requireBuilderBytes(t, ax.ReadFile(envLogPath))
-
- envLines := core.Split(core.Trim(string(envContent)), "\n")
- if !stdlibAssertContains(envLines, "BAR=baz") {
- t.Fatalf("expected %v to contain %v", envLines, "BAR=baz")
- }
- if !stdlibAssertContains(envLines, "FOO=bar") {
- t.Fatalf("expected %v to contain %v", envLines, "FOO=bar")
- }
- if !stdlibAssertContains(envLines, "TARGET_OS="+runtime.GOOS) {
- t.Fatalf("expected %v to contain %v", envLines, "TARGET_OS="+runtime.GOOS)
- }
- if !stdlibAssertContains(envLines, "TARGET_ARCH="+runtime.GOARCH) {
- t.Fatalf("expected %v to contain %v", envLines, "TARGET_ARCH="+runtime.GOARCH)
- }
- if !stdlibAssertContains(envLines, "OUTPUT_DIR="+outputDir) {
- t.Fatalf("expected %v to contain %v", envLines, "OUTPUT_DIR="+outputDir)
- }
- if !stdlibAssertContains(envLines, "TARGET_DIR="+ax.Join(outputDir, runtime.GOOS+"_"+runtime.GOARCH)) {
- t.Fatalf("expected %v to contain %v", envLines, "TARGET_DIR="+ax.Join(outputDir, runtime.GOOS+"_"+runtime.GOARCH))
- }
- if !stdlibAssertContains(envLines, "GOOS="+runtime.GOOS) {
- t.Fatalf("expected %v to contain %v", envLines, "GOOS="+runtime.GOOS)
- }
- if !stdlibAssertContains(envLines, "GOARCH="+runtime.GOARCH) {
- t.Fatalf("expected %v to contain %v", envLines, "GOARCH="+runtime.GOARCH)
- }
- if !stdlibAssertContains(envLines, "NAME=envflags") {
- t.Fatalf("expected %v to contain %v", envLines, "NAME=envflags")
- }
- if !stdlibAssertContains(envLines, "VERSION=v1.2.3") {
- t.Fatalf("expected %v to contain %v", envLines, "VERSION=v1.2.3")
- }
- if !stdlibAssertContains(envLines, "CGO_ENABLED=0") {
- t.Fatalf("expected %v to contain %v", envLines, "CGO_ENABLED=0")
- }
-
- })
-
- t.Run("applies configured cache paths to go cache env vars", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
- logDir := t.TempDir()
- envLogPath := ax.Join(logDir, "go-cache-env.log")
-
- t.Setenv("GO_BUILD_ENV_LOG_FILE", envLogPath)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "cachetest",
- Cache: build.CacheConfig{
- Enabled: true,
- Paths: []string{
- ax.Join(outputDir, "cache", "go-build"),
- ax.Join(outputDir, "cache", "go-mod"),
- },
- },
- }
- targets := []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- envContent := requireBuilderBytes(t, ax.ReadFile(envLogPath))
-
- envLines := core.Split(core.Trim(string(envContent)), "\n")
- if !stdlibAssertContains(envLines, "GOCACHE="+ax.Join(outputDir, "cache", "go-build")) {
- t.Fatalf("expected %v to contain %v", envLines, "GOCACHE="+ax.Join(outputDir, "cache", "go-build"))
- }
- if !stdlibAssertContains(envLines, "GOMODCACHE="+ax.Join(outputDir, "cache", "go-mod")) {
- t.Fatalf("expected %v to contain %v", envLines, "GOMODCACHE="+ax.Join(outputDir, "cache", "go-mod"))
- }
-
- })
-
- t.Run("passes build tags through to go build", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "go-tags.log")
- t.Setenv("GO_BUILD_LOG_FILE", logPath)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "tagged",
- BuildTags: []string{"webkit2_41", "integration"},
- }
- targets := []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- args := core.Split(core.Trim(string(content)), "\n")
- if stdlibAssertEmpty(args) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertEqual("build", args[0]) {
- t.Fatalf("want %v, got %v", "build", args[0])
- }
- if !stdlibAssertContains(args, "-tags") {
- t.Fatalf("expected %v to contain %v", args, "-tags")
- }
- if !stdlibAssertContains(args, "webkit2_41,integration") {
- t.Fatalf("expected %v to contain %v", args, "webkit2_41,integration")
- }
-
- })
-
- t.Run("injects version into ldflags and environment", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
- argsLogPath := ax.Join(t.TempDir(), "go-version-args.log")
- envLogPath := ax.Join(t.TempDir(), "go-version-env.log")
-
- t.Setenv("GO_BUILD_LOG_FILE", argsLogPath)
- t.Setenv("GO_BUILD_ENV_LOG_FILE", envLogPath)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "versioned",
- Version: "v1.2.3",
- }
- targets := []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- argsContent := requireBuilderBytes(t, ax.ReadFile(argsLogPath))
-
- args := core.Split(core.Trim(string(argsContent)), "\n")
- if stdlibAssertEmpty(args) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertContains(args, "-ldflags") {
- t.Fatalf("expected %v to contain %v", args, "-ldflags")
- }
- if !stdlibAssertContains(args, "-X main.version=v1.2.3") {
- t.Fatalf("expected %v to contain %v", args, "-X main.version=v1.2.3")
- }
-
- envContent := requireBuilderBytes(t, ax.ReadFile(envLogPath))
-
- envLines := core.Split(core.Trim(string(envContent)), "\n")
- if !stdlibAssertContains(envLines, "VERSION=v1.2.3") {
- t.Fatalf("expected %v to contain %v", envLines, "VERSION=v1.2.3")
- }
-
- })
-
- t.Run("uses garble when obfuscation is enabled", func(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("garble test helper uses a shell script")
- }
-
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
- logDir := t.TempDir()
- logPath := ax.Join(logDir, "garble.log")
-
- t.Setenv("GARBLE_LOG_FILE", logPath)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "obfuscated",
- Obfuscate: true,
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- args := core.Split(core.Trim(string(content)), "\n")
- if stdlibAssertEmpty(args) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertEqual("build", args[0]) {
- t.Fatalf("want %v, got %v", "build", args[0])
- }
- if !stdlibAssertContains(args, "-trimpath") {
- t.Fatalf("expected %v to contain %v", args, "-trimpath")
- }
- if !stdlibAssertContains(args, "-o") {
- t.Fatalf("expected %v to contain %v", args, "-o")
- }
- if !stdlibAssertContains(args, ".") {
- t.Fatalf("expected %v to contain %v", args, ".")
- }
-
- })
-
- t.Run("finds garble in GOBIN when it is not on PATH", func(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("garble test helper uses a shell script")
- }
-
- goDir := t.TempDir()
- setupFakeGoBinary(t, goDir)
- t.Setenv("PATH", goDir+string(core.PathListSeparator)+"/usr/bin"+string(core.PathListSeparator)+"/bin")
-
- garbleDir := t.TempDir()
- setupFakeGarbleBinary(t, garbleDir)
- t.Setenv("GOBIN", garbleDir)
-
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
- logDir := t.TempDir()
- logPath := ax.Join(logDir, "garble-gobin.log")
-
- t.Setenv("GARBLE_LOG_FILE", logPath)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "obfuscated-gobin",
- Obfuscate: true,
- }
- targets := []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- args := core.Split(core.Trim(string(content)), "\n")
- if stdlibAssertEmpty(args) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertEqual("build", args[0]) {
- t.Fatalf("want %v, got %v", "build", args[0])
- }
- if !stdlibAssertContains(args, "-trimpath") {
- t.Fatalf("expected %v to contain %v", args, "-trimpath")
- }
-
- })
-
- t.Run("builds the configured main package path", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- if result := ax.MkdirAll(ax.Join(projectDir, "cmd", "myapp"), 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(projectDir, "cmd", "myapp", "main.go"), []byte("package main\n\nfunc main() {}\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "go-build-args.log")
- t.Setenv("GO_BUILD_LOG_FILE", logPath)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "mainpackage",
- }
- cfg.Project.Main = "./cmd/myapp"
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- args := core.Split(core.Trim(string(content)), "\n")
- if stdlibAssertEmpty(args) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertContains(args, "./cmd/myapp") {
- t.Fatalf("expected %v to contain %v", args, "./cmd/myapp")
- }
- if stdlibAssertContains(args, ".") {
- t.Fatalf("expected %v not to contain %v", args, ".")
- }
-
- })
-
- t.Run("creates output directory if missing", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
- outputDir := ax.Join(t.TempDir(), "nested", "output")
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "nestedtest",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
- if !storage.Local.IsDir(outputDir) {
- t.Fatalf("expected directory to exist: %v", outputDir)
- }
-
- })
-
- t.Run("defaults output directory to project dist when not specified", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- Name: "defaultoutput",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- expectedDir := ax.Join(projectDir, "dist")
- if !storage.Local.IsDir(expectedDir) {
- t.Fatalf("expected directory to exist: %v", expectedDir)
- }
- if !stdlibAssertContains(artifacts[0].Path, expectedDir) {
- t.Fatalf("expected %v to contain %v", artifacts[0].Path, expectedDir)
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- })
-}
-
-func TestGo_GoBuilderBuildBad(t *testing.T) {
- binDir := t.TempDir()
- setupFakeBuildToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- t.Run("returns error for nil config", func(t *testing.T) {
- builder := NewGoBuilder()
-
- result := builder.Build(context.Background(), nil, []build.Target{{OS: "linux", Arch: "amd64"}})
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "config is nil") {
- t.Fatalf("expected %v to contain %v", result.Error(), "config is nil")
- }
-
- })
-
- t.Run("defaults to current platform when targets are empty", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "test",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(runtime.GOOS, artifacts[0].OS) {
- t.Fatalf("want %v, got %v", runtime.GOOS, artifacts[0].OS)
- }
- if !stdlibAssertEqual(runtime.GOARCH, artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", runtime.GOARCH, artifacts[0].Arch)
- }
- if result := ax.Stat(artifacts[0].Path); !result.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- })
-
- t.Run("returns error for invalid project directory", func(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: "/nonexistent/path",
- OutputDir: t.TempDir(),
- Name: "test",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- result := builder.Build(context.Background(), cfg, targets)
- if result.OK {
- t.Fatal("expected error")
- }
-
- })
-
- t.Run("returns error for invalid Go code", func(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- dir := t.TempDir()
-
- // Create go.mod
- if result := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test\n\ngo 1.21"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(dir, "main.go"), []byte("this is not valid go code"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: dir,
- OutputDir: t.TempDir(),
- Name: "test",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- result := builder.Build(context.Background(), cfg, targets)
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "go build failed") {
- t.Fatalf("expected %v to contain %v", result.Error(), "go build failed")
- }
-
- })
-
- t.Run("returns partial artifacts on partial failure", func(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- // Create a project that will fail on one target
- // Using an invalid arch for linux
- projectDir := setupGoTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "partialtest",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH}, // This should succeed
- {OS: "linux", Arch: "invalid_arch"}, // This should fail
- }
-
- result := builder.Build(context.Background(), cfg, targets)
- if result.OK {
- t.Fatal("expected error")
- }
- if stdlibAssertEmpty(result.Error()) {
- t.Fatal("expected non-empty error")
- }
-
- })
-
- t.Run("respects context cancellation", func(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- projectDir := setupGoTestProject(t)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "canceltest",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- // Create an already cancelled context
- ctx, cancel := context.WithCancel(context.Background())
- cancel()
-
- result := builder.Build(ctx, cfg, targets)
- if result.OK {
- t.Fatal("expected error")
- }
-
- })
-
- t.Run("rejects unsafe version identifiers before invoking go build", func(t *testing.T) {
- projectDir := setupGoTestProject(t)
-
- builder := NewGoBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "unsafe-version",
- Version: "v1.2.3;rm -rf /",
- }
-
- result := builder.Build(context.Background(), cfg, []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}})
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "unsupported characters") {
- t.Fatalf("expected %v to contain %v", result.Error(), "unsupported characters")
- }
-
- })
-}
-
-func TestGo_GoBuilderResolveGarbleCliGood(t *testing.T) {
- t.Run("returns an explicit fallback path when it exists", func(t *testing.T) {
- builder := NewGoBuilder()
- garblePath := ax.Join(t.TempDir(), "garble")
- if result := ax.WriteFile(garblePath, []byte("#!/bin/sh\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", t.TempDir())
-
- command := requireCPPString(t, builder.resolveGarbleCli(garblePath))
- if !stdlibAssertEqual(garblePath, command) {
- t.Fatalf("want %v, got %v", garblePath, command)
- }
-
- })
-}
-
-func TestGo_GoBuilderResolveGarbleCliBad(t *testing.T) {
- t.Run("returns an error when garble cannot be resolved", func(t *testing.T) {
- builder := NewGoBuilder()
- t.Setenv("PATH", t.TempDir())
-
- result := builder.resolveGarbleCli(ax.Join(t.TempDir(), "missing-garble"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "garble CLI not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "garble CLI not found")
- }
-
- })
-}
-
-func TestGo_GarbleInstallPathsUgly(t *testing.T) {
- gobin := ax.Join(t.TempDir(), "gobin")
- gopathOne := ax.Join(t.TempDir(), "gopath-one")
- gopathTwo := ax.Join(t.TempDir(), "gopath-two")
-
- t.Setenv("GOBIN", gobin)
- t.Setenv("GOPATH", gopathOne+string(core.PathListSeparator)+" "+string(core.PathListSeparator)+gopathTwo)
-
- paths := garbleInstallPaths()
- if !stdlibAssertEqual([]string{ax.Join(gobin, "garble"), ax.Join(gopathOne, "bin", "garble"), ax.Join(gopathTwo, "bin", "garble")}, paths) {
- t.Fatalf("want %v, got %v", []string{ax.Join(gobin, "garble"), ax.Join(gopathOne, "bin", "garble"), ax.Join(gopathTwo, "bin", "garble")}, paths)
- }
-
-}
-
-func TestGo_hasVersionLDFlag_Good(t *testing.T) {
- if !(hasVersionLDFlag([]string{"-s", "-w", "-X main.version=v1.2.3"})) {
- t.Fatal("expected true")
- }
- if !(hasVersionLDFlag([]string{"-X main.Version=v1.2.3"})) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestGo_hasVersionLDFlag_Bad(t *testing.T) {
- if hasVersionLDFlag([]string{"-s", "-w"}) {
- t.Fatal("expected false")
- }
-
-}
-
-func TestGo_containsString_Ugly(t *testing.T) {
- if !(containsString([]string{"alpha", "beta"}, "beta")) {
- t.Fatal("expected true")
- }
- if containsString([]string{"alpha", "beta"}, "gamma") {
- t.Fatal("expected false")
- }
-
-}
-
-func TestGo_GoBuilderInterfaceGood(t *testing.T) {
- builder := NewGoBuilder()
- var _ build.Builder = builder
- if !stdlibAssertEqual("go", builder.Name()) {
- t.Fatalf("want %v, got %v", "go", builder.Name())
- }
- detected := requireCPPBool(t, builder.Detect(nil, t.TempDir()))
- if detected {
- t.Fatal("expected empty temp directory not to be detected")
- }
-}
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
-
-// --- v0.9.0 generated compliance triplets ---
-func TestGo_NewGoBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewGoBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestGo_NewGoBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewGoBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestGo_NewGoBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewGoBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestGo_GoBuilder_Name_Good(t *core.T) {
- subject := &GoBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestGo_GoBuilder_Name_Bad(t *core.T) {
- subject := &GoBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestGo_GoBuilder_Name_Ugly(t *core.T) {
- subject := &GoBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestGo_GoBuilder_Detect_Good(t *core.T) {
- subject := &GoBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestGo_GoBuilder_Detect_Bad(t *core.T) {
- subject := &GoBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestGo_GoBuilder_Detect_Ugly(t *core.T) {
- subject := &GoBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestGo_GoBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &GoBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestGo_GoBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &GoBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestGo_GoBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &GoBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/linuxkit.go b/pkg/build/builders/linuxkit.go
deleted file mode 100644
index 197c0c9..0000000
--- a/pkg/build/builders/linuxkit.go
+++ /dev/null
@@ -1,324 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- stdfs "io/fs"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// LinuxKitBuilder builds LinuxKit images.
-//
-// b := builders.NewLinuxKitBuilder()
-type LinuxKitBuilder struct{}
-
-// NewLinuxKitBuilder creates a new LinuxKit builder.
-//
-// b := builders.NewLinuxKitBuilder()
-func NewLinuxKitBuilder() *LinuxKitBuilder {
- return &LinuxKitBuilder{}
-}
-
-// Name returns the builder's identifier.
-//
-// name := b.Name() // → "linuxkit"
-func (b *LinuxKitBuilder) Name() string {
- return "linuxkit"
-}
-
-// Detect checks if a linuxkit.yml, linuxkit.yaml, or nested YAML config exists in the directory.
-//
-// result := b.Detect(storage.Local, ".")
-func (b *LinuxKitBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(build.IsLinuxKitProject(fs, dir))
-}
-
-// Build builds LinuxKit images for the specified targets.
-//
-// result := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
-func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("LinuxKitBuilder.Build", "config is nil", nil))
- }
- filesystem := ensureBuildFilesystem(cfg)
- artifactFilesystem := build.ResolveOutputMedium(cfg)
-
- linuxkitCommandResult := b.resolveLinuxKitCli()
- if !linuxkitCommandResult.OK {
- return linuxkitCommandResult
- }
- linuxkitCommand := linuxkitCommandResult.Value.(string)
-
- // Determine config file path
- configPath := cfg.LinuxKitConfig
- if configPath == "" {
- // Auto-detect
- if filesystem.IsFile(ax.Join(cfg.ProjectDir, "linuxkit.yml")) {
- configPath = ax.Join(cfg.ProjectDir, "linuxkit.yml")
- } else if filesystem.IsFile(ax.Join(cfg.ProjectDir, "linuxkit.yaml")) {
- configPath = ax.Join(cfg.ProjectDir, "linuxkit.yaml")
- } else {
- // Look in .core/linuxkit/
- lkDir := ax.Join(cfg.ProjectDir, ".core", "linuxkit")
- if filesystem.IsDir(lkDir) {
- entriesResult := filesystem.List(lkDir)
- if entriesResult.OK {
- entries := entriesResult.Value.([]stdfs.DirEntry)
- for _, entry := range entries {
- if entry.IsDir() {
- continue
- }
- name := entry.Name()
- if core.HasSuffix(name, ".yml") || core.HasSuffix(name, ".yaml") {
- configPath = ax.Join(lkDir, entry.Name())
- break
- }
- }
- }
- }
- }
- } else if !ax.IsAbs(configPath) {
- configPath = ax.Join(cfg.ProjectDir, configPath)
- }
-
- if configPath == "" {
- return core.Fail(core.E("LinuxKitBuilder.Build", "no LinuxKit config file found. Specify with --config or create linuxkit.yml", nil))
- }
-
- // Validate config file exists
- if !filesystem.IsFile(configPath) {
- return core.Fail(core.E("LinuxKitBuilder.Build", "config file not found: "+configPath, nil))
- }
-
- // Determine output formats
- formats := cfg.Formats
- if len(formats) == 0 {
- formats = []string{"qcow2-bios"} // Default to QEMU-compatible format
- }
-
- // Create output directory
- outputDir := cfg.OutputDir
- if outputDir == "" && build.MediumIsLocal(artifactFilesystem) {
- outputDir = defaultOutputDir(cfg)
- }
- created := ensureOutputDir(artifactFilesystem, outputDir, "LinuxKitBuilder.Build")
- if !created.OK {
- return created
- }
-
- stageResult := prepareStagedOutput(outputDir, artifactFilesystem, "core-build-linuxkit-*", "LinuxKitBuilder.Build")
- if !stageResult.OK {
- return stageResult
- }
- stage := stageResult.Value.(stagedOutput)
- defer stage.cleanup()
-
- // Determine base name from config file or project name
- baseName := cfg.Name
- if baseName == "" {
- baseName = core.TrimSuffix(ax.Base(configPath), ".yml")
- baseName = core.TrimSuffix(baseName, ".yaml")
- }
-
- // If no targets, default to linux/amd64
- targets = defaultLinuxTargets(targets)
-
- var artifacts []build.Artifact
-
- // Build for each target and format
- for _, target := range targets {
- // LinuxKit only supports Linux
- if target.OS != "linux" {
- core.Print(nil, "Skipping %s/%s (LinuxKit only supports Linux)", target.OS, target.Arch)
- continue
- }
-
- for _, format := range formats {
- outputName := core.Sprintf("%s-%s", baseName, target.Arch)
-
- args := b.buildLinuxKitArgs(configPath, format, outputName, stage.commandOutputDir, target.Arch)
-
- core.Print(nil, "Building LinuxKit image: %s (%s, %s)", outputName, format, target.Arch)
- executed := ax.ExecWithEnv(ctx, cfg.ProjectDir, build.BuildEnvironment(cfg), linuxkitCommand, args...)
- if !executed.OK {
- return core.Fail(core.E("LinuxKitBuilder.Build", "build failed for "+target.Arch+"/"+format, core.NewError(executed.Error())))
- }
-
- // Determine the actual output file path
- artifactPath := b.getArtifactPath(stage.commandOutputDir, outputName, format)
-
- // Verify the artifact was created
- if !stage.commandFS.Exists(artifactPath) {
- // Try alternate naming conventions
- artifactPath = b.findArtifact(stage.commandFS, stage.commandOutputDir, outputName, format)
- if artifactPath == "" {
- return core.Fail(core.E("LinuxKitBuilder.Build", "artifact not found after build: expected "+b.getArtifactPath(stage.commandOutputDir, outputName, format), nil))
- }
- }
-
- finalArtifactPath := b.getArtifactPath(outputDir, outputName, format)
- if artifactPath != finalArtifactPath {
- copied := build.CopyMediumPath(stage.commandFS, artifactPath, artifactFilesystem, finalArtifactPath)
- if !copied.OK {
- return copied
- }
- }
-
- artifacts = append(artifacts, build.Artifact{
- Path: finalArtifactPath,
- OS: target.OS,
- Arch: target.Arch,
- })
- }
- }
-
- return core.Ok(artifacts)
-}
-
-// buildLinuxKitArgs builds the arguments for linuxkit build command.
-func (b *LinuxKitBuilder) buildLinuxKitArgs(configPath, format, outputName, outputDir, arch string) []string {
- args := []string{"build"}
-
- // Output format
- args = append(args, "--format", format)
-
- // Output name
- args = append(args, "--name", outputName)
-
- // Output directory
- args = append(args, "--dir", outputDir)
-
- // Architecture (if not amd64)
- if arch != "amd64" {
- args = append(args, "--arch", arch)
- }
-
- // Config file
- args = append(args, configPath)
-
- return args
-}
-
-// getArtifactPath returns the expected path of the built artifact.
-func (b *LinuxKitBuilder) getArtifactPath(outputDir, outputName, format string) string {
- ext := b.getFormatExtension(format)
- if outputDir == "" {
- return outputName + ext
- }
- return ax.Join(outputDir, outputName+ext)
-}
-
-// findArtifact searches for the built artifact with various naming conventions.
-func (b *LinuxKitBuilder) findArtifact(fs storage.Medium, outputDir, outputName, format string) string {
- // LinuxKit can create files with different suffixes
- extensions := []string{
- b.getFormatExtension(format),
- "-bios" + b.getFormatExtension(format),
- "-efi" + b.getFormatExtension(format),
- }
-
- for _, ext := range extensions {
- path := outputName + ext
- if outputDir != "" {
- path = ax.Join(outputDir, outputName+ext)
- }
- if fs.Exists(path) {
- return path
- }
- }
-
- // Try to find any file matching the output name
- entriesResult := fs.List(outputDir)
- if entriesResult.OK {
- entries := entriesResult.Value.([]stdfs.DirEntry)
- for _, entry := range entries {
- if core.HasPrefix(entry.Name(), outputName) {
- match := entry.Name()
- if outputDir != "" {
- match = ax.Join(outputDir, entry.Name())
- }
- // Return first match that looks like an image
- if isLinuxKitArtifact(match) {
- return match
- }
- }
- }
- }
-
- return ""
-}
-
-// getFormatExtension returns the file extension for a LinuxKit output format.
-func (b *LinuxKitBuilder) getFormatExtension(format string) string {
- switch format {
- case "iso", "iso-bios", "iso-efi":
- return ".iso"
- case "raw", "raw-bios", "raw-efi":
- return ".raw"
- case "qcow2", "qcow2-bios", "qcow2-efi":
- return ".qcow2"
- case "vmdk":
- return ".vmdk"
- case "vhd":
- return ".vhd"
- case "gcp":
- return ".img.tar.gz"
- case "aws":
- return ".raw"
- case "docker":
- return ".docker.tar"
- case "tar":
- return ".tar"
- case "kernel+initrd":
- return "-initrd.img"
- default:
- return "." + core.TrimSuffix(format, "-bios")
- }
-}
-
-// isLinuxKitArtifact reports whether a file path looks like a LinuxKit build output.
-func isLinuxKitArtifact(path string) bool {
- switch {
- case core.HasSuffix(path, ".img.tar.gz"):
- return true
- case core.HasSuffix(path, ".docker.tar"):
- return true
- case core.HasSuffix(path, "-initrd.img"):
- return true
- case core.HasSuffix(path, ".tar"):
- return true
- case core.HasSuffix(path, ".iso"):
- return true
- case core.HasSuffix(path, ".qcow2"):
- return true
- case core.HasSuffix(path, ".raw"):
- return true
- case core.HasSuffix(path, ".vmdk"):
- return true
- case core.HasSuffix(path, ".vhd"):
- return true
- default:
- return false
- }
-}
-
-// resolveLinuxKitCli returns the executable path for the linuxkit CLI.
-func (b *LinuxKitBuilder) resolveLinuxKitCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/linuxkit",
- "/opt/homebrew/bin/linuxkit",
- }
- }
-
- command := ax.ResolveCommand("linuxkit", paths...)
- if !command.OK {
- return core.Fail(core.E("LinuxKitBuilder.resolveLinuxKitCli", "linuxkit CLI not found. Install with: brew install linuxkit (macOS) or see https://github.com/linuxkit/linuxkit", core.NewError(command.Error())))
- }
-
- return command
-}
diff --git a/pkg/build/builders/linuxkit_example_test.go b/pkg/build/builders/linuxkit_example_test.go
deleted file mode 100644
index fed47fe..0000000
--- a/pkg/build/builders/linuxkit_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewLinuxKitBuilder references NewLinuxKitBuilder on this package API surface.
-func ExampleNewLinuxKitBuilder() {
- _ = NewLinuxKitBuilder
- core.Println("NewLinuxKitBuilder")
- // Output: NewLinuxKitBuilder
-}
-
-// ExampleLinuxKitBuilder_Name references LinuxKitBuilder.Name on this package API surface.
-func ExampleLinuxKitBuilder_Name() {
- _ = (*LinuxKitBuilder).Name
- core.Println("LinuxKitBuilder.Name")
- // Output: LinuxKitBuilder.Name
-}
-
-// ExampleLinuxKitBuilder_Detect references LinuxKitBuilder.Detect on this package API surface.
-func ExampleLinuxKitBuilder_Detect() {
- _ = (*LinuxKitBuilder).Detect
- core.Println("LinuxKitBuilder.Detect")
- // Output: LinuxKitBuilder.Detect
-}
-
-// ExampleLinuxKitBuilder_Build references LinuxKitBuilder.Build on this package API surface.
-func ExampleLinuxKitBuilder_Build() {
- _ = (*LinuxKitBuilder).Build
- core.Println("LinuxKitBuilder.Build")
- // Output: LinuxKitBuilder.Build
-}
diff --git a/pkg/build/builders/linuxkit_image.go b/pkg/build/builders/linuxkit_image.go
deleted file mode 100644
index 2748bc8..0000000
--- a/pkg/build/builders/linuxkit_image.go
+++ /dev/null
@@ -1,503 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- "text/template" // AX-6 intrinsic: no core template primitive.
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// LinuxKitImageBuilder renders and builds immutable LinuxKit base images.
-type LinuxKitImageBuilder struct{}
-
-// LinuxKitImageTemplateData is the template input for embedded immutable image definitions.
-type LinuxKitImageTemplateData struct {
- Name string
- Description string
- Version string
- GPU bool
- Mounts []string
- ServiceImage string
- EntrypointCommand string
-}
-
-// NewLinuxKitImageBuilder creates an immutable LinuxKit image builder.
-func NewLinuxKitImageBuilder() *LinuxKitImageBuilder {
- return &LinuxKitImageBuilder{}
-}
-
-// Name returns the builder identifier.
-func (b *LinuxKitImageBuilder) Name() string {
- return "linuxkit-image"
-}
-
-// ListBaseImages returns the built-in immutable LinuxKit base images.
-func (b *LinuxKitImageBuilder) ListBaseImages() []build.LinuxKitBaseImage {
- return build.LinuxKitBaseImages()
-}
-
-// ArtifactPath returns the final output path for a requested immutable image format.
-func (b *LinuxKitImageBuilder) ArtifactPath(outputDir, name, format string) string {
- if outputDir == "" {
- return name + b.outputExtension(format)
- }
- return ax.Join(outputDir, name+b.outputExtension(format))
-}
-
-// Build renders the embedded LinuxKit template and emits one artifact per format.
-func (b *LinuxKitImageBuilder) Build(ctx context.Context, cfg *build.Config) core.Result {
- if cfg == nil {
- return core.Fail(core.E("LinuxKitImageBuilder.Build", "build config is required", nil))
- }
-
- ensureBuildFilesystem(cfg)
- artifactFilesystem := build.ResolveOutputMedium(cfg)
-
- imageCfg := mergeLinuxKitImageConfig(build.DefaultLinuxKitConfig(), cfg.LinuxKit)
- baseImage, ok := build.LookupLinuxKitBaseImage(imageCfg.Base)
- if !ok {
- return core.Fail(core.E("LinuxKitImageBuilder.Build", "unknown LinuxKit image base: "+imageCfg.Base, nil))
- }
-
- outputDir := cfg.OutputDir
- if outputDir == "" && build.MediumIsLocal(artifactFilesystem) {
- outputDir = defaultOutputDir(cfg)
- }
- if outputDir != "" && !ax.IsAbs(outputDir) && cfg.ProjectDir != "" && build.MediumIsLocal(artifactFilesystem) {
- outputDir = ax.Join(cfg.ProjectDir, outputDir)
- }
- created := ensureOutputDir(artifactFilesystem, outputDir, "LinuxKitImageBuilder.Build")
- if !created.OK {
- return created
- }
-
- stageResult := prepareStagedOutput(outputDir, artifactFilesystem, "core-build-linuxkit-image-*", "LinuxKitImageBuilder.Build")
- if !stageResult.OK {
- return stageResult
- }
- stage := stageResult.Value.(stagedOutput)
- defer stage.cleanup()
-
- imageName := cfg.Name
- if imageName == "" {
- imageName = imageCfg.Base
- }
-
- serviceImageResult := b.prepareServiceImage(ctx, cfg.ProjectDir, imageName, cfg.Version, baseImage, imageCfg)
- if !serviceImageResult.OK {
- return serviceImageResult
- }
- serviceImage := serviceImageResult.Value.(linuxKitServiceImageBuild)
- defer serviceImage.cleanup()
-
- renderedTemplateResult := b.renderTemplate(baseImage, imageCfg, cfg.Version, serviceImage.image)
- if !renderedTemplateResult.OK {
- return renderedTemplateResult
- }
- renderedTemplate := renderedTemplateResult.Value.(string)
-
- templatePath := ax.Join(stage.commandOutputDir, "."+imageName+"-linuxkit.yml")
- written := stage.commandFS.WriteMode(templatePath, renderedTemplate, 0o644)
- if !written.OK {
- return core.Fail(core.E("LinuxKitImageBuilder.Build", "failed to write LinuxKit template", core.NewError(written.Error())))
- }
- defer func() { stage.commandFS.Delete(templatePath) }()
-
- linuxkitCommandResult := (&LinuxKitBuilder{}).resolveLinuxKitCli()
- if !linuxkitCommandResult.OK {
- return linuxkitCommandResult
- }
- linuxkitCommand := linuxkitCommandResult.Value.(string)
-
- formats := imageCfg.Formats
- if len(formats) == 0 {
- formats = append([]string(nil), build.DefaultLinuxKitConfig().Formats...)
- }
-
- artifacts := make([]build.Artifact, 0, len(formats))
- for _, format := range formats {
- if format == "" {
- continue
- }
-
- artifactPathResult := b.buildFormat(ctx, stage.commandFS, artifactFilesystem, linuxkitCommand, cfg.ProjectDir, stage.commandOutputDir, outputDir, imageName, templatePath, format)
- if !artifactPathResult.OK {
- return artifactPathResult
- }
- artifactPath := artifactPathResult.Value.(string)
-
- artifacts = append(artifacts, build.Artifact{
- Path: artifactPath,
- OS: "linux",
- Arch: core.Env("ARCH"),
- })
- }
-
- return core.Ok(artifacts)
-}
-
-func mergeLinuxKitImageConfig(defaults, override build.LinuxKitConfig) build.LinuxKitConfig {
- cfg := defaults
- if override.Base != "" {
- cfg.Base = override.Base
- }
- if override.Packages != nil {
- cfg.Packages = append([]string(nil), override.Packages...)
- }
- if override.Mounts != nil {
- cfg.Mounts = append([]string(nil), override.Mounts...)
- }
- cfg.GPU = override.GPU
- if override.Formats != nil {
- cfg.Formats = append([]string(nil), override.Formats...)
- }
- if override.Registry != "" {
- cfg.Registry = override.Registry
- }
- return normalizeLinuxKitImageConfig(cfg)
-}
-
-func normalizeLinuxKitImageConfig(cfg build.LinuxKitConfig) build.LinuxKitConfig {
- defaults := build.DefaultLinuxKitConfig()
-
- cfg.Base = core.Trim(cfg.Base)
- if cfg.Base == "" {
- cfg.Base = defaults.Base
- }
-
- cfg.Registry = core.Trim(cfg.Registry)
- cfg.Packages = uniqueStrings(cfg.Packages)
- cfg.Mounts = uniqueStrings(cfg.Mounts)
- if len(cfg.Mounts) == 0 {
- cfg.Mounts = append([]string(nil), defaults.Mounts...)
- }
-
- cfg.Formats = normalizeLinuxKitImageFormats(cfg.Formats)
- if len(cfg.Formats) == 0 {
- cfg.Formats = append([]string(nil), defaults.Formats...)
- }
-
- return cfg
-}
-
-func normalizeLinuxKitImageFormats(values []string) []string {
- if len(values) == 0 {
- return values
- }
-
- result := make([]string, 0, len(values))
- seen := make(map[string]struct{}, len(values))
- for _, value := range values {
- value = core.Lower(core.Trim(value))
- if value == "" {
- continue
- }
- if _, ok := seen[value]; ok {
- continue
- }
- seen[value] = struct{}{}
- result = append(result, value)
- }
-
- return result
-}
-
-func (b *LinuxKitImageBuilder) renderTemplate(baseImage build.LinuxKitBaseImage, cfg build.LinuxKitConfig, version, serviceImage string) core.Result {
- cfg = normalizeLinuxKitImageConfig(cfg)
-
- templateContentResult := build.LinuxKitBaseTemplate(baseImage.Name)
- if !templateContentResult.OK {
- return templateContentResult
- }
- templateContent := templateContentResult.Value.(string)
-
- tmpl, parseFailure := template.New(baseImage.Name).Parse(templateContent)
- if parseFailure != nil {
- return core.Fail(core.E("LinuxKitImageBuilder.renderTemplate", "failed to parse embedded LinuxKit template", parseFailure))
- }
-
- if version == "" {
- version = "dev"
- }
-
- data := LinuxKitImageTemplateData{
- Name: baseImage.Name,
- Description: baseImage.Description,
- Version: version,
- GPU: cfg.GPU,
- Mounts: uniqueStrings(cfg.Mounts),
- ServiceImage: serviceImage,
- EntrypointCommand: "tail -f /dev/null",
- }
-
- rendered := core.NewBuffer()
- if renderFailure := tmpl.Execute(rendered, data); renderFailure != nil {
- return core.Fail(core.E("LinuxKitImageBuilder.renderTemplate", "failed to render LinuxKit template", renderFailure))
- }
-
- return core.Ok(rendered.String())
-}
-
-type linuxKitServiceImageBuild struct {
- image string
- cleanup func()
-}
-
-func (b *LinuxKitImageBuilder) prepareServiceImage(ctx context.Context, projectDir, imageName, version string, baseImage build.LinuxKitBaseImage, cfg build.LinuxKitConfig) core.Result {
- cfg = normalizeLinuxKitImageConfig(cfg)
-
- dockerCommandResult := (&DockerBuilder{}).resolveDockerCli()
- if !dockerCommandResult.OK {
- return core.Fail(core.E("LinuxKitImageBuilder.prepareServiceImage", "failed to resolve docker CLI for immutable service image build", core.NewError(dockerCommandResult.Error())))
- }
- dockerCommand := dockerCommandResult.Value.(string)
-
- tempDirResult := ax.TempDir("core-build-linuxkit-service-*")
- if !tempDirResult.OK {
- return core.Fail(core.E("LinuxKitImageBuilder.prepareServiceImage", "failed to create service image build context", core.NewError(tempDirResult.Error())))
- }
- tempDir := tempDirResult.Value.(string)
-
- cleanup := func() {
- ax.RemoveAll(tempDir)
- }
-
- contentHash := linuxKitServiceImageContentHash(baseImage, cfg)
- serviceImage := buildLinuxKitServiceImageReference(imageName, version)
- mounts := uniqueStrings(append([]string{"/workspace"}, cfg.Mounts...))
- dockerfile := renderLinuxKitServiceDockerfile(
- imageName,
- version,
- baseImage.Version,
- contentHash,
- append(append([]string{}, baseImage.DefaultPackages...), cfg.Packages...),
- mounts,
- cfg.GPU,
- )
- dockerfileWritten := ax.WriteString(ax.Join(tempDir, "Dockerfile"), dockerfile, 0o644)
- if !dockerfileWritten.OK {
- cleanup()
- return core.Fail(core.E("LinuxKitImageBuilder.prepareServiceImage", "failed to write service image Dockerfile", core.NewError(dockerfileWritten.Error())))
- }
-
- built := ax.ExecDir(ctx, tempDir, dockerCommand, "build", "-t", serviceImage, ".")
- if !built.OK {
- cleanup()
- return core.Fail(core.E("LinuxKitImageBuilder.prepareServiceImage", "failed to build immutable LinuxKit service image", core.NewError(built.Error())))
- }
-
- return core.Ok(linuxKitServiceImageBuild{image: serviceImage, cleanup: cleanup})
-}
-
-func renderLinuxKitServiceDockerfile(imageName, version, baseVersion, contentHash string, packages, mounts []string, gpu bool) string {
- lines := []string{
- "FROM alpine:3.19",
- }
-
- packages = uniqueStrings(packages)
- if len(packages) > 0 {
- lines = append(lines, "RUN apk add --no-cache "+core.Join(" ", packages...))
- }
-
- mounts = uniqueStrings(append([]string{"/workspace"}, mounts...))
- if len(mounts) > 0 {
- lines = append(lines, "RUN mkdir -p "+core.Join(" ", mounts...))
- }
-
- if gpu {
- lines = append(lines, "RUN mkdir -p /etc/profile.d && printf 'export CORE_GPU=1\\n' > /etc/profile.d/core-gpu.sh")
- }
-
- lines = append(lines,
- "WORKDIR /workspace",
- "LABEL org.opencontainers.image.title="+imageName,
- "LABEL org.opencontainers.image.version="+normalizeLinuxKitServiceVersionTag(version),
- "LABEL dappcore.core-build.base-version="+normalizeLinuxKitServiceTag(baseVersion),
- "LABEL dappcore.core-build.content-hash="+normalizeLinuxKitServiceTag(contentHash),
- "ENV CORE_IMAGE="+imageName,
- "ENV CORE_IMAGE_VERSION="+normalizeLinuxKitServiceVersionTag(version),
- "ENV CORE_IMAGE_BASE_VERSION="+normalizeLinuxKitServiceTag(baseVersion),
- "ENV CORE_IMAGE_CONTENT_HASH="+normalizeLinuxKitServiceTag(contentHash),
- core.Sprintf("ENV CORE_GPU=%d", boolToInt(gpu)),
- `CMD ["/bin/sh", "-lc", "tail -f /dev/null"]`,
- )
-
- return core.Join("\n", lines...) + "\n"
-}
-
-func buildLinuxKitServiceImageReference(imageName, version string) string {
- tag := normalizeLinuxKitServiceVersionTag(version)
- return core.Sprintf("core-build-linuxkit/%s:%s", imageName, tag)
-}
-
-func linuxKitServiceImageContentHash(baseImage build.LinuxKitBaseImage, cfg build.LinuxKitConfig) string {
- cfg = normalizeLinuxKitImageConfig(cfg)
- parts := []string{
- baseImage.Name,
- baseImage.Version,
- core.Join(",", uniqueStrings(baseImage.DefaultPackages)...),
- core.Join(",", uniqueStrings(cfg.Packages)...),
- core.Join(",", uniqueStrings(cfg.Mounts)...),
- core.Sprintf("%t", cfg.GPU),
- }
- sum := core.SHA256([]byte(core.Join("\n", parts...)))
- return core.HexEncode(sum[:6])
-}
-
-func normalizeLinuxKitServiceVersionTag(value string) string {
- value = core.Trim(value)
- value = core.TrimPrefix(value, "v")
- if value == "" {
- value = "dev"
- }
- return normalizeLinuxKitServiceTag(value)
-}
-
-func normalizeLinuxKitServiceTag(value string) string {
- value = core.Lower(core.Trim(value))
- value = core.Replace(value, "/", "-")
- value = core.Replace(value, "\\", "-")
- value = core.Replace(value, ":", "-")
- value = core.Replace(value, " ", "-")
- value = core.Replace(value, "\t", "-")
- value = core.Replace(value, "_", "-")
- value = core.Replace(value, "..", ".")
- value = trimLinuxKitServiceTagBoundary(value)
- if value == "" {
- return "latest"
- }
- return value
-}
-
-func trimLinuxKitServiceTagBoundary(value string) string {
- for value != "" {
- switch {
- case core.HasPrefix(value, "-"):
- value = core.TrimPrefix(value, "-")
- case core.HasPrefix(value, "."):
- value = core.TrimPrefix(value, ".")
- case core.HasSuffix(value, "-"):
- value = core.TrimSuffix(value, "-")
- case core.HasSuffix(value, "."):
- value = core.TrimSuffix(value, ".")
- default:
- return value
- }
- }
- return value
-}
-
-func boolToInt(value bool) int {
- if value {
- return 1
- }
- return 0
-}
-
-func uniqueStrings(values []string) []string {
- if len(values) == 0 {
- return values
- }
-
- result := make([]string, 0, len(values))
- seen := make(map[string]struct{}, len(values))
- for _, value := range values {
- value = core.Trim(value)
- if value == "" {
- continue
- }
- if _, ok := seen[value]; ok {
- continue
- }
- seen[value] = struct{}{}
- result = append(result, value)
- }
- return result
-}
-
-func (b *LinuxKitImageBuilder) buildFormat(ctx context.Context, commandFilesystem storage.Medium, artifactFilesystem storage.Medium, linuxkitCommand, projectDir, commandOutputDir, outputDir, imageName, templatePath, format string) core.Result {
- linuxKitFormat := b.linuxKitFormat(format)
- buildName := imageName
- if format == "apple" {
- buildName = imageName + "-apple"
- }
-
- args := []string{
- "build",
- "--format", linuxKitFormat,
- "--name", buildName,
- "--dir", commandOutputDir,
- templatePath,
- }
-
- executed := ax.ExecWithEnv(ctx, projectDir, nil, linuxkitCommand, args...)
- if !executed.OK {
- return core.Fail(core.E("LinuxKitImageBuilder.Build", "build failed for "+format, core.NewError(executed.Error())))
- }
-
- builtPath := ax.Join(commandOutputDir, buildName+b.intermediateExtension(format))
- commandFinalPath := b.ArtifactPath(commandOutputDir, imageName, format)
- finalPath := b.ArtifactPath(outputDir, imageName, format)
-
- if format == "apple" {
- if !commandFilesystem.Exists(builtPath) {
- return core.Fail(core.E("LinuxKitImageBuilder.Build", "apple container artifact not found: "+builtPath, nil))
- }
- renamed := commandFilesystem.Rename(builtPath, commandFinalPath)
- if !renamed.OK {
- return core.Fail(core.E("LinuxKitImageBuilder.Build", "failed to rename Apple container artifact", core.NewError(renamed.Error())))
- }
- if commandFinalPath != finalPath {
- copied := build.CopyMediumPath(commandFilesystem, commandFinalPath, artifactFilesystem, finalPath)
- if !copied.OK {
- return copied
- }
- }
- return core.Ok(finalPath)
- }
-
- if !commandFilesystem.Exists(commandFinalPath) {
- return core.Fail(core.E("LinuxKitImageBuilder.Build", "artifact not found after build: "+commandFinalPath, nil))
- }
- if commandFinalPath != finalPath {
- copied := build.CopyMediumPath(commandFilesystem, commandFinalPath, artifactFilesystem, finalPath)
- if !copied.OK {
- return copied
- }
- }
-
- return core.Ok(finalPath)
-}
-
-func (b *LinuxKitImageBuilder) linuxKitFormat(format string) string {
- switch format {
- case "oci", "apple":
- return "tar"
- default:
- return format
- }
-}
-
-func (b *LinuxKitImageBuilder) intermediateExtension(format string) string {
- switch format {
- case "oci", "apple":
- return ".tar"
- default:
- return b.outputExtension(format)
- }
-}
-
-func (b *LinuxKitImageBuilder) outputExtension(format string) string {
- switch format {
- case "oci":
- return ".tar"
- case "apple":
- return ".aci"
- default:
- return (&LinuxKitBuilder{}).getFormatExtension(format)
- }
-}
diff --git a/pkg/build/builders/linuxkit_image_example_test.go b/pkg/build/builders/linuxkit_image_example_test.go
deleted file mode 100644
index 4a5807b..0000000
--- a/pkg/build/builders/linuxkit_image_example_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewLinuxKitImageBuilder references NewLinuxKitImageBuilder on this package API surface.
-func ExampleNewLinuxKitImageBuilder() {
- _ = NewLinuxKitImageBuilder
- core.Println("NewLinuxKitImageBuilder")
- // Output: NewLinuxKitImageBuilder
-}
-
-// ExampleLinuxKitImageBuilder_Name references LinuxKitImageBuilder.Name on this package API surface.
-func ExampleLinuxKitImageBuilder_Name() {
- _ = (*LinuxKitImageBuilder).Name
- core.Println("LinuxKitImageBuilder.Name")
- // Output: LinuxKitImageBuilder.Name
-}
-
-// ExampleLinuxKitImageBuilder_ListBaseImages references LinuxKitImageBuilder.ListBaseImages on this package API surface.
-func ExampleLinuxKitImageBuilder_ListBaseImages() {
- _ = (*LinuxKitImageBuilder).ListBaseImages
- core.Println("LinuxKitImageBuilder.ListBaseImages")
- // Output: LinuxKitImageBuilder.ListBaseImages
-}
-
-// ExampleLinuxKitImageBuilder_ArtifactPath references LinuxKitImageBuilder.ArtifactPath on this package API surface.
-func ExampleLinuxKitImageBuilder_ArtifactPath() {
- _ = (*LinuxKitImageBuilder).ArtifactPath
- core.Println("LinuxKitImageBuilder.ArtifactPath")
- // Output: LinuxKitImageBuilder.ArtifactPath
-}
-
-// ExampleLinuxKitImageBuilder_Build references LinuxKitImageBuilder.Build on this package API surface.
-func ExampleLinuxKitImageBuilder_Build() {
- _ = (*LinuxKitImageBuilder).Build
- core.Println("LinuxKitImageBuilder.Build")
- // Output: LinuxKitImageBuilder.Build
-}
diff --git a/pkg/build/builders/linuxkit_image_test.go b/pkg/build/builders/linuxkit_image_test.go
deleted file mode 100644
index a069498..0000000
--- a/pkg/build/builders/linuxkit_image_test.go
+++ /dev/null
@@ -1,372 +0,0 @@
-package builders
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func setupFakeLinuxKitImageToolchain(t *testing.T, binDir string) {
- t.Helper()
-
- dockerScript := `#!/bin/sh
-exit 0
-`
- if result := ax.WriteFile(ax.Join(binDir, "docker"), []byte(dockerScript), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- script := `#!/bin/sh
-set -eu
-
-format=""
-dir=""
-name=""
-while [ $# -gt 0 ]; do
- case "$1" in
- build)
- ;;
- --format)
- shift
- format="${1:-}"
- ;;
- --dir)
- shift
- dir="${1:-}"
- ;;
- --name)
- shift
- name="${1:-}"
- ;;
- esac
- shift
-done
-
-ext=".img"
-case "$format" in
- tar)
- ext=".tar"
- ;;
- iso|iso-bios|iso-efi)
- ext=".iso"
- ;;
- raw|raw-bios|raw-efi)
- ext=".raw"
- ;;
- qcow2|qcow2-bios|qcow2-efi)
- ext=".qcow2"
- ;;
-esac
-
-mkdir -p "$dir"
-printf 'linuxkit image\n' > "$dir/$name$ext"
-`
- if result := ax.WriteFile(ax.Join(binDir, "linuxkit"), []byte(script), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func TestLinuxKitImage_LinuxKitImageBuilderNameGood(t *testing.T) {
- builder := NewLinuxKitImageBuilder()
- if !stdlibAssertEqual("linuxkit-image", builder.Name()) {
- t.Fatalf("want %v, got %v", "linuxkit-image", builder.Name())
- }
-
-}
-
-func TestLinuxKitImage_LinuxKitImageBuilderArtifactPathGood(t *testing.T) {
- builder := NewLinuxKitImageBuilder()
- if !stdlibAssertEqual("/dist/core-dev.tar", builder.ArtifactPath("/dist", "core-dev", "oci")) {
- t.Fatalf("want %v, got %v", "/dist/core-dev.tar", builder.ArtifactPath("/dist", "core-dev", "oci"))
- }
- if !stdlibAssertEqual("/dist/core-dev.aci", builder.ArtifactPath("/dist", "core-dev", "apple")) {
- t.Fatalf("want %v, got %v", "/dist/core-dev.aci", builder.ArtifactPath("/dist", "core-dev", "apple"))
- }
- if !stdlibAssertEqual("/dist/core-dev.iso", builder.ArtifactPath("/dist", "core-dev", "iso")) {
- t.Fatalf("want %v, got %v", "/dist/core-dev.iso", builder.ArtifactPath("/dist", "core-dev", "iso"))
- }
-
-}
-
-func TestLinuxKitImage_BuildLinuxKitServiceImageReference_UsesVersionTagGood(t *testing.T) {
- if !stdlibAssertEqual("core-build-linuxkit/core-dev:1.2.3", buildLinuxKitServiceImageReference("core-dev", "v1.2.3")) {
- t.Fatalf("want %v, got %v", "core-build-linuxkit/core-dev:1.2.3", buildLinuxKitServiceImageReference("core-dev", "v1.2.3"))
- }
- if !stdlibAssertEqual("core-build-linuxkit/core-dev:dev", buildLinuxKitServiceImageReference("core-dev", "")) {
- t.Fatalf("want %v, got %v", "core-build-linuxkit/core-dev:dev", buildLinuxKitServiceImageReference("core-dev", ""))
- }
-
-}
-
-func TestLinuxKitImage_RenderLinuxKitServiceDockerfile_IncludesMetadataGood(t *testing.T) {
- rendered := renderLinuxKitServiceDockerfile("core-dev", "v1.2.3", "2026.04.08", "abc123", []string{"git"}, []string{"/workspace"}, false)
- if !stdlibAssertContains(rendered, "LABEL org.opencontainers.image.version=1.2.3") {
- t.Fatalf("expected %v to contain %v", rendered, "LABEL org.opencontainers.image.version=1.2.3")
- }
- if !stdlibAssertContains(rendered, "LABEL dappcore.core-build.content-hash=abc123") {
- t.Fatalf("expected %v to contain %v", rendered, "LABEL dappcore.core-build.content-hash=abc123")
- }
- if !stdlibAssertContains(rendered, "ENV CORE_IMAGE_VERSION=1.2.3") {
- t.Fatalf("expected %v to contain %v", rendered, "ENV CORE_IMAGE_VERSION=1.2.3")
- }
- if !stdlibAssertContains(rendered, "ENV CORE_IMAGE_CONTENT_HASH=abc123") {
- t.Fatalf("expected %v to contain %v", rendered, "ENV CORE_IMAGE_CONTENT_HASH=abc123")
- }
-
-}
-
-func TestLinuxKitImage_RenderTemplateUsesImmutableServiceImageGood(t *testing.T) {
- builder := NewLinuxKitImageBuilder()
- baseImage, ok := build.LookupLinuxKitBaseImage("core-dev")
- if !(ok) {
- t.Fatal("expected true")
- }
-
- renderResult := builder.renderTemplate(baseImage, build.LinuxKitConfig{
- Base: "core-dev",
- Mounts: []string{"/workspace"},
- Formats: []string{"oci"},
- Packages: []string{"gh"},
- }, "v1.2.3", "core-build-linuxkit/core-dev:test")
- if !renderResult.OK {
- t.Fatalf("unexpected error: %v", renderResult.Error())
- }
- rendered := renderResult.Value.(string)
- if !stdlibAssertContains(rendered, `image: "core-build-linuxkit/core-dev:test"`) {
- t.Fatalf("expected %v to contain %v", rendered, `image: "core-build-linuxkit/core-dev:test"`)
- }
- if !stdlibAssertContains(rendered, "tail -f /dev/null") {
- t.Fatalf("expected %v to contain %v", rendered, "tail -f /dev/null")
- }
- if stdlibAssertContains(rendered, "apk add --no-cache") {
- t.Fatalf("expected %v not to contain %v", rendered, "apk add --no-cache")
- }
-
-}
-
-func TestLinuxKitImage_RenderTemplateRestoresDefaultWorkspaceMountGood(t *testing.T) {
- builder := NewLinuxKitImageBuilder()
- baseImage, ok := build.LookupLinuxKitBaseImage("core-dev")
- if !(ok) {
- t.Fatal("expected true")
- }
-
- renderResult := builder.renderTemplate(baseImage, build.LinuxKitConfig{
- Base: "core-dev",
- Mounts: []string{""},
- Formats: []string{"oci"},
- }, "v1.2.3", "core-build-linuxkit/core-dev:test")
- if !renderResult.OK {
- t.Fatalf("unexpected error: %v", renderResult.Error())
- }
- rendered := renderResult.Value.(string)
- if !stdlibAssertContains(rendered, "binds:") {
- t.Fatalf("expected %v to contain %v", rendered, "binds:")
- }
- if !stdlibAssertContains(rendered, "- /workspace:/workspace") {
- t.Fatalf("expected %v to contain %v", rendered, "- /workspace:/workspace")
- }
-
-}
-
-func TestLinuxKitImage_LinuxKitImageBuilderBuildGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeLinuxKitImageToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- outputDir := t.TempDir()
-
- builder := NewLinuxKitImageBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "core-dev",
- Version: "v1.2.3",
- LinuxKit: build.LinuxKitConfig{
- Base: "core-dev",
- Packages: []string{"gh"},
- Mounts: []string{"/workspace"},
- Formats: []string{"oci", "apple"},
- },
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg))
- if len(artifacts) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(artifacts))
- }
- if result := ax.Stat(ax.Join(outputDir, "core-dev.tar")); !result.OK {
- t.Fatalf("expected file to exist: %v", ax.Join(outputDir, "core-dev.tar"))
- }
- if result := ax.Stat(ax.Join(outputDir, "core-dev.aci")); !result.OK {
- t.Fatalf("expected file to exist: %v", ax.Join(outputDir, "core-dev.aci"))
- }
- if ax.Exists(ax.Join(outputDir, ".core-dev-linuxkit.yml")) {
- t.Fatalf("expected file not to exist: %v", ax.Join(outputDir, ".core-dev-linuxkit.yml"))
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestLinuxkitImage_NewLinuxKitImageBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewLinuxKitImageBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_NewLinuxKitImageBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewLinuxKitImageBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_NewLinuxKitImageBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewLinuxKitImageBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_Name_Good(t *core.T) {
- subject := &LinuxKitImageBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_Name_Bad(t *core.T) {
- subject := &LinuxKitImageBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_Name_Ugly(t *core.T) {
- subject := &LinuxKitImageBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_ListBaseImages_Good(t *core.T) {
- subject := &LinuxKitImageBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ListBaseImages()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_ListBaseImages_Bad(t *core.T) {
- subject := &LinuxKitImageBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ListBaseImages()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_ListBaseImages_Ugly(t *core.T) {
- subject := &LinuxKitImageBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ListBaseImages()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_ArtifactPath_Good(t *core.T) {
- subject := &LinuxKitImageBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ArtifactPath(core.Path(t.TempDir(), "go-build-compliance"), "agent", "tar.gz")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_ArtifactPath_Bad(t *core.T) {
- subject := &LinuxKitImageBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ArtifactPath("", "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_ArtifactPath_Ugly(t *core.T) {
- subject := &LinuxKitImageBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ArtifactPath(core.Path(t.TempDir(), "go-build-compliance"), "agent", "tar.gz")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &LinuxKitImageBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &LinuxKitImageBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_LinuxKitImageBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &LinuxKitImageBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/linuxkit_test.go b/pkg/build/builders/linuxkit_test.go
deleted file mode 100644
index 6d18b8f..0000000
--- a/pkg/build/builders/linuxkit_test.go
+++ /dev/null
@@ -1,663 +0,0 @@
-package builders
-
-import (
- "context"
- "testing"
-
- "dappco.re/go/build/internal/ax"
-
- core "dappco.re/go"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func setupFakeLinuxKitToolchain(t *testing.T, binDir string) {
- t.Helper()
-
- script := `#!/bin/sh
-set -eu
-
-if [ "${1:-}" != "build" ]; then
- exit 1
-fi
-
-config=""
-dir=""
-name=""
-while [ $# -gt 0 ]; do
- if [ "$1" = "--dir" ]; then
- shift
- dir="${1:-}"
- elif [ "$1" = "--name" ]; then
- shift
- name="${1:-}"
- fi
- shift
-done
-
-if [ -n "$dir" ] && [ -n "$name" ]; then
- mkdir -p "$dir"
- printf 'linuxkit image\n' > "$dir/$name.iso"
-fi
-`
- if result := ax.WriteFile(ax.Join(binDir, "linuxkit"), []byte(script), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func TestLinuxKit_LinuxKitBuilderNameGood(t *testing.T) {
- builder := NewLinuxKitBuilder()
- if !stdlibAssertEqual("linuxkit", builder.Name()) {
- t.Fatalf("want %v, got %v", "linuxkit", builder.Name())
- }
-
-}
-
-func TestLinuxKit_LinuxKitBuilderDetectGood(t *testing.T) {
- fs := storage.Local
-
- t.Run("detects linuxkit.yml in root", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "linuxkit.yml"), []byte("kernel:\n image: test\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects linuxkit.yaml in root", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "linuxkit.yaml"), []byte("kernel:\n image: test\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects .core/linuxkit/*.yml", func(t *testing.T) {
- dir := t.TempDir()
- lkDir := ax.Join(dir, ".core", "linuxkit")
- if result := ax.MkdirAll(lkDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(lkDir, "server.yml"), []byte("kernel:\n image: test\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects .core/linuxkit/*.yaml", func(t *testing.T) {
- dir := t.TempDir()
- lkDir := ax.Join(dir, ".core", "linuxkit")
- if result := ax.MkdirAll(lkDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(lkDir, "server.yaml"), []byte("kernel:\n image: test\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects .core/linuxkit with multiple yml files", func(t *testing.T) {
- dir := t.TempDir()
- lkDir := ax.Join(dir, ".core", "linuxkit")
- if result := ax.MkdirAll(lkDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(lkDir, "server.yml"), []byte("kernel:\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(lkDir, "desktop.yml"), []byte("kernel:\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for empty directory", func(t *testing.T) {
- dir := t.TempDir()
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false for non-LinuxKit project", func(t *testing.T) {
- dir := t.TempDir()
- if result := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false for empty .core/linuxkit directory", func(t *testing.T) {
- dir := t.TempDir()
- lkDir := ax.Join(dir, ".core", "linuxkit")
- if result := ax.MkdirAll(lkDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false when .core/linuxkit has only non-yml files", func(t *testing.T) {
- dir := t.TempDir()
- lkDir := ax.Join(dir, ".core", "linuxkit")
- if result := ax.MkdirAll(lkDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(lkDir, "README.md"), []byte("# LinuxKit\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false when .core/linuxkit has only non-yaml files", func(t *testing.T) {
- dir := t.TempDir()
- lkDir := ax.Join(dir, ".core", "linuxkit")
- if result := ax.MkdirAll(lkDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(lkDir, "README.md"), []byte("# LinuxKit\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("ignores subdirectories in .core/linuxkit", func(t *testing.T) {
- dir := t.TempDir()
- lkDir := ax.Join(dir, ".core", "linuxkit")
- subDir := ax.Join(lkDir, "subdir")
- if result := ax.MkdirAll(subDir, 0755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- if result := ax.WriteFile(ax.Join(subDir, "server.yml"), []byte("kernel:\n"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewLinuxKitBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestLinuxKit_LinuxKitBuilderGetFormatExtensionGood(t *testing.T) {
- builder := NewLinuxKitBuilder()
-
- tests := []struct {
- format string
- expected string
- }{
- {"iso", ".iso"},
- {"iso-bios", ".iso"},
- {"iso-efi", ".iso"},
- {"raw", ".raw"},
- {"raw-bios", ".raw"},
- {"raw-efi", ".raw"},
- {"qcow2", ".qcow2"},
- {"qcow2-bios", ".qcow2"},
- {"qcow2-efi", ".qcow2"},
- {"vmdk", ".vmdk"},
- {"vhd", ".vhd"},
- {"gcp", ".img.tar.gz"},
- {"aws", ".raw"},
- {"docker", ".docker.tar"},
- {"tar", ".tar"},
- {"kernel+initrd", "-initrd.img"},
- {"custom", ".custom"},
- }
-
- for _, tc := range tests {
- t.Run(tc.format, func(t *testing.T) {
- ext := builder.getFormatExtension(tc.format)
- if !stdlibAssertEqual(tc.expected, ext) {
- t.Fatalf("want %v, got %v", tc.expected, ext)
- }
-
- })
- }
-}
-
-func TestLinuxKit_LinuxKitBuilderGetArtifactPathGood(t *testing.T) {
- builder := NewLinuxKitBuilder()
-
- t.Run("constructs correct path", func(t *testing.T) {
- path := builder.getArtifactPath("/dist", "server-amd64", "iso")
- if !stdlibAssertEqual("/dist/server-amd64.iso", path) {
- t.Fatalf("want %v, got %v", "/dist/server-amd64.iso", path)
- }
-
- })
-
- t.Run("constructs correct path for qcow2", func(t *testing.T) {
- path := builder.getArtifactPath("/output/linuxkit", "server-arm64", "qcow2-bios")
- if !stdlibAssertEqual("/output/linuxkit/server-arm64.qcow2", path) {
- t.Fatalf("want %v, got %v", "/output/linuxkit/server-arm64.qcow2", path)
- }
-
- })
-
- t.Run("constructs correct path for docker images", func(t *testing.T) {
- path := builder.getArtifactPath("/output/linuxkit", "server-amd64", "docker")
- if !stdlibAssertEqual("/output/linuxkit/server-amd64.docker.tar", path) {
- t.Fatalf("want %v, got %v", "/output/linuxkit/server-amd64.docker.tar", path)
- }
-
- })
-
- t.Run("constructs correct path for kernel+initrd images", func(t *testing.T) {
- path := builder.getArtifactPath("/output/linuxkit", "server-amd64", "kernel+initrd")
- if !stdlibAssertEqual("/output/linuxkit/server-amd64-initrd.img", path) {
- t.Fatalf("want %v, got %v", "/output/linuxkit/server-amd64-initrd.img", path)
- }
-
- })
-}
-
-func TestLinuxKit_LinuxKitBuilderBuildLinuxKitArgsGood(t *testing.T) {
- builder := NewLinuxKitBuilder()
-
- t.Run("builds args for amd64 without --arch", func(t *testing.T) {
- args := builder.buildLinuxKitArgs("/config.yml", "iso", "output", "/dist", "amd64")
- if !stdlibAssertContains(args, "build") {
- t.Fatalf("expected %v to contain %v", args, "build")
- }
- if !stdlibAssertContains(args, "--format") {
- t.Fatalf("expected %v to contain %v", args, "--format")
- }
- if !stdlibAssertContains(args, "iso") {
- t.Fatalf("expected %v to contain %v", args, "iso")
- }
- if !stdlibAssertContains(args, "--name") {
- t.Fatalf("expected %v to contain %v", args, "--name")
- }
- if !stdlibAssertContains(args, "output") {
- t.Fatalf("expected %v to contain %v", args, "output")
- }
- if !stdlibAssertContains(args, "--dir") {
- t.Fatalf("expected %v to contain %v", args, "--dir")
- }
- if !stdlibAssertContains(args, "/dist") {
- t.Fatalf("expected %v to contain %v", args, "/dist")
- }
- if !stdlibAssertContains(args, "/config.yml") {
- t.Fatalf("expected %v to contain %v", args, "/config.yml")
- }
- if stdlibAssertContains(args, "--arch") {
- t.Fatalf("expected %v not to contain %v", args, "--arch")
- }
-
- })
-
- t.Run("builds args for arm64 with --arch", func(t *testing.T) {
- args := builder.buildLinuxKitArgs("/config.yml", "qcow2", "output", "/dist", "arm64")
- if !stdlibAssertContains(args, "--arch") {
- t.Fatalf("expected %v to contain %v", args, "--arch")
- }
- if !stdlibAssertContains(args, "arm64") {
- t.Fatalf("expected %v to contain %v", args, "arm64")
- }
-
- })
-}
-
-func TestLinuxKit_LinuxKitBuilderBuild_ResolvesRelativeConfigPathGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeLinuxKitToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- configPath := ax.Join(projectDir, "deploy", "linuxkit.yml")
- if result := ax.MkdirAll(ax.Dir(configPath), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result := ax.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0o644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- builder := NewLinuxKitBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "sample",
- LinuxKitConfig: "deploy/linuxkit.yml",
- Formats: []string{"iso"},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- expectedPath := ax.Join(outputDir, "sample-amd64.iso")
- if !stdlibAssertEqual(expectedPath, artifacts[0].Path) {
- t.Fatalf("want %v, got %v", expectedPath, artifacts[0].Path)
- }
- if result := ax.Stat(expectedPath); !result.OK {
- t.Fatalf("expected file to exist: %v", expectedPath)
- }
-
-}
-
-func TestLinuxKit_LinuxKitBuilderFindArtifactGood(t *testing.T) {
- fs := storage.Local
- builder := NewLinuxKitBuilder()
-
- t.Run("finds artifact with exact extension", func(t *testing.T) {
- dir := t.TempDir()
- artifactPath := ax.Join(dir, "server-amd64.iso")
- if result := ax.WriteFile(artifactPath, []byte("fake iso"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- found := builder.findArtifact(fs, dir, "server-amd64", "iso")
- if !stdlibAssertEqual(artifactPath, found) {
- t.Fatalf("want %v, got %v", artifactPath, found)
- }
-
- })
-
- t.Run("returns empty for missing artifact", func(t *testing.T) {
- dir := t.TempDir()
-
- found := builder.findArtifact(fs, dir, "nonexistent", "iso")
- if !stdlibAssertEmpty(found) {
- t.Fatalf("expected empty, got %v", found)
- }
-
- })
-
- t.Run("finds artifact with alternate naming", func(t *testing.T) {
- dir := t.TempDir()
- // Create file matching the name prefix + known image extension
- artifactPath := ax.Join(dir, "server-amd64.qcow2")
- if result := ax.WriteFile(artifactPath, []byte("fake qcow2"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- found := builder.findArtifact(fs, dir, "server-amd64", "qcow2")
- if !stdlibAssertEqual(artifactPath, found) {
- t.Fatalf("want %v, got %v", artifactPath, found)
- }
-
- })
-
- t.Run("finds cloud image artifacts", func(t *testing.T) {
- dir := t.TempDir()
- artifactPath := ax.Join(dir, "server-amd64-gcp.img.tar.gz")
- if result := ax.WriteFile(artifactPath, []byte("fake gcp image"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- found := builder.findArtifact(fs, dir, "server-amd64", "gcp")
- if !stdlibAssertEqual(artifactPath, found) {
- t.Fatalf("want %v, got %v", artifactPath, found)
- }
-
- })
-
- t.Run("finds docker artifacts", func(t *testing.T) {
- dir := t.TempDir()
- artifactPath := ax.Join(dir, "server-amd64.docker.tar")
- if result := ax.WriteFile(artifactPath, []byte("fake docker tar"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- found := builder.findArtifact(fs, dir, "server-amd64", "docker")
- if !stdlibAssertEqual(artifactPath, found) {
- t.Fatalf("want %v, got %v", artifactPath, found)
- }
-
- })
-
- t.Run("finds kernel+initrd artifacts", func(t *testing.T) {
- dir := t.TempDir()
- artifactPath := ax.Join(dir, "server-amd64-initrd.img")
- if result := ax.WriteFile(artifactPath, []byte("fake initrd"), 0644); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- found := builder.findArtifact(fs, dir, "server-amd64", "kernel+initrd")
- if !stdlibAssertEqual(artifactPath, found) {
- t.Fatalf("want %v, got %v", artifactPath, found)
- }
-
- })
-}
-
-func TestLinuxKit_LinuxKitBuilderInterfaceGood(t *testing.T) {
- builder := NewLinuxKitBuilder()
- var _ build.Builder = builder
- if !stdlibAssertEqual("linuxkit", builder.Name()) {
- t.Fatalf("want %v, got %v", "linuxkit", builder.Name())
- }
- detected := requireCPPBool(t, builder.Detect(nil, t.TempDir()))
- if detected {
- t.Fatal("expected empty temp directory not to be detected")
- }
-}
-
-func TestLinuxKit_LinuxKitBuilderResolveLinuxKitCliGood(t *testing.T) {
- builder := NewLinuxKitBuilder()
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "linuxkit")
- if result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- command := requireCPPString(t, builder.resolveLinuxKitCli(fallbackPath))
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestLinuxKit_LinuxKitBuilderResolveLinuxKitCliBad(t *testing.T) {
- builder := NewLinuxKitBuilder()
- t.Setenv("PATH", "")
-
- result := builder.resolveLinuxKitCli(ax.Join(t.TempDir(), "missing-linuxkit"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "linuxkit CLI not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "linuxkit CLI not found")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestLinuxkit_NewLinuxKitBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewLinuxKitBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkit_NewLinuxKitBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewLinuxKitBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkit_NewLinuxKitBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewLinuxKitBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkit_LinuxKitBuilder_Name_Good(t *core.T) {
- subject := &LinuxKitBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkit_LinuxKitBuilder_Name_Bad(t *core.T) {
- subject := &LinuxKitBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkit_LinuxKitBuilder_Name_Ugly(t *core.T) {
- subject := &LinuxKitBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkit_LinuxKitBuilder_Detect_Good(t *core.T) {
- subject := &LinuxKitBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkit_LinuxKitBuilder_Detect_Bad(t *core.T) {
- subject := &LinuxKitBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkit_LinuxKitBuilder_Detect_Ugly(t *core.T) {
- subject := &LinuxKitBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkit_LinuxKitBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &LinuxKitBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkit_LinuxKitBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &LinuxKitBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkit_LinuxKitBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &LinuxKitBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/node.go b/pkg/build/builders/node.go
deleted file mode 100644
index c768b4f..0000000
--- a/pkg/build/builders/node.go
+++ /dev/null
@@ -1,338 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- stdfs "io/fs"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// NodeBuilder builds Node.js projects with the detected package manager.
-//
-// b := builders.NewNodeBuilder()
-type NodeBuilder struct{}
-
-// NewNodeBuilder creates a new NodeBuilder instance.
-//
-// b := builders.NewNodeBuilder()
-func NewNodeBuilder() *NodeBuilder {
- return &NodeBuilder{}
-}
-
-// Name returns the builder's identifier.
-//
-// name := b.Name() // → "node"
-func (b *NodeBuilder) Name() string {
- return "node"
-}
-
-// Detect checks if this builder can handle the project in the given directory.
-//
-// ok, err := b.Detect(storage.Local, ".")
-func (b *NodeBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(build.IsNodeProject(fs, dir))
-}
-
-// Build runs the project build script once per target and collects artifacts
-// from the target-specific output directory.
-//
-// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
-func (b *NodeBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("NodeBuilder.Build", "config is nil", nil))
- }
- filesystem := ensureBuildFilesystem(cfg)
-
- targets = defaultRuntimeTargets(targets, runtime.GOOS, runtime.GOARCH)
-
- outputDir := cfg.OutputDir
- if outputDir == "" {
- outputDir = defaultOutputDir(cfg)
- }
- created := ensureOutputDir(filesystem, outputDir, "NodeBuilder.Build")
- if !created.OK {
- return created
- }
-
- projectDir := b.resolveNodeProjectDir(filesystem, cfg.ProjectDir)
- if projectDir == "" {
- projectDir = cfg.ProjectDir
- }
-
- commandResult := b.resolveBuildCommand(cfg, filesystem, projectDir)
- if !commandResult.OK {
- return commandResult
- }
- spec := commandResult.Value.(commandSpec)
- command := spec.command
- args := spec.args
-
- var artifacts []build.Artifact
- for _, target := range targets {
- platformDirResult := ensurePlatformDir(filesystem, outputDir, target, "NodeBuilder.Build")
- if !platformDirResult.OK {
- return platformDirResult
- }
- platformDir := platformDirResult.Value.(string)
-
- env := configuredTargetEnv(cfg, target, standardTargetValues(outputDir, platformDir, target)...)
-
- output := ax.CombinedOutput(ctx, projectDir, env, command, args...)
- if !output.OK {
- return core.Fail(core.E("NodeBuilder.Build", command+" build failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- found := b.findArtifactsForTarget(cfg.FS, outputDir, target)
- artifacts = append(artifacts, found...)
- }
-
- return core.Ok(artifacts)
-}
-
-// resolveNodeProjectDir locates the directory containing package.json.
-// It prefers the project root, then searches nested directories to depth 2.
-func (b *NodeBuilder) resolveNodeProjectDir(fs storage.Medium, projectDir string) string {
- if b.hasNodeManifest(fs, projectDir) {
- return projectDir
- }
-
- return b.findNodeProjectDir(fs, projectDir, 0)
-}
-
-// findNodeProjectDir searches for a package.json within nested directories.
-func (b *NodeBuilder) findNodeProjectDir(fs storage.Medium, dir string, depth int) string {
- if depth >= 2 {
- return ""
- }
-
- entriesResult := fs.List(dir)
- if !entriesResult.OK {
- return ""
- }
- entries := entriesResult.Value.([]stdfs.DirEntry)
-
- for _, entry := range entries {
- if !entry.IsDir() {
- continue
- }
-
- name := entry.Name()
- if name == "node_modules" || core.HasPrefix(name, ".") {
- continue
- }
-
- candidateDir := ax.Join(dir, name)
- if b.hasNodeManifest(fs, candidateDir) {
- return candidateDir
- }
-
- if nested := b.findNodeProjectDir(fs, candidateDir, depth+1); nested != "" {
- return nested
- }
- }
-
- return ""
-}
-
-func (b *NodeBuilder) hasNodeManifest(fs storage.Medium, dir string) bool {
- return fs.IsFile(ax.Join(dir, "package.json")) || b.hasDenoConfig(fs, dir)
-}
-
-func (b *NodeBuilder) hasDenoConfig(fs storage.Medium, dir string) bool {
- return fs.IsFile(ax.Join(dir, "deno.json")) || fs.IsFile(ax.Join(dir, "deno.jsonc"))
-}
-
-// resolvePackageManager selects the package manager from lockfiles.
-//
-// packageManager := b.resolvePackageManager(storage.Local, ".")
-func (b *NodeBuilder) resolvePackageManager(fs storage.Medium, projectDir string) core.Result {
- if declared := detectDeclaredPackageManager(fs, projectDir); declared != "" {
- return core.Ok(declared)
- }
-
- switch {
- case fs.IsFile(ax.Join(projectDir, "bun.lockb")) || fs.IsFile(ax.Join(projectDir, "bun.lock")):
- return core.Ok("bun")
- case fs.IsFile(ax.Join(projectDir, "pnpm-lock.yaml")):
- return core.Ok("pnpm")
- case fs.IsFile(ax.Join(projectDir, "yarn.lock")):
- return core.Ok("yarn")
- case fs.IsFile(ax.Join(projectDir, "package-lock.json")):
- return core.Ok("npm")
- default:
- return core.Ok("npm")
- }
-}
-
-// resolveBuildCommand returns the executable and arguments for the selected package manager.
-//
-// command, args, err := b.resolveBuildCommand("npm")
-func (b *NodeBuilder) resolveBuildCommand(cfg *build.Config, fs storage.Medium, projectDir string) core.Result {
- configuredDenoBuild := ""
- if cfg != nil {
- configuredDenoBuild = cfg.DenoBuild
- }
-
- if b.hasDenoConfig(fs, projectDir) || build.DenoRequested(configuredDenoBuild) {
- return resolveDenoBuildCommand(cfg, b.resolveDenoCli)
- }
-
- if build.NpmRequested(configuredNpmBuild(cfg)) {
- return resolveNpmBuildCommand(cfg, b.resolveNpmCli)
- }
-
- packageManagerResult := b.resolvePackageManager(fs, projectDir)
- if !packageManagerResult.OK {
- return packageManagerResult
- }
- packageManager := packageManagerResult.Value.(string)
-
- var paths []string
- switch packageManager {
- case "bun":
- paths = []string{"/usr/local/bin/bun", "/opt/homebrew/bin/bun"}
- case "pnpm":
- paths = []string{"/usr/local/bin/pnpm", "/opt/homebrew/bin/pnpm"}
- case "yarn":
- paths = []string{"/usr/local/bin/yarn", "/opt/homebrew/bin/yarn"}
- default:
- paths = []string{"/usr/local/bin/npm", "/opt/homebrew/bin/npm"}
- packageManager = "npm"
- }
-
- command := ax.ResolveCommand(packageManager, paths...)
- if !command.OK {
- return core.Fail(core.E("NodeBuilder.resolveBuildCommand", packageManager+" CLI not found", core.NewError(command.Error())))
- }
-
- switch packageManager {
- case "yarn":
- return core.Ok(commandSpec{command: command.Value.(string), args: []string{"build"}})
- default:
- return core.Ok(commandSpec{command: command.Value.(string), args: []string{"run", "build"}})
- }
-}
-
-func configuredNpmBuild(cfg *build.Config) string {
- if cfg == nil {
- return ""
- }
- return cfg.NpmBuild
-}
-
-func (b *NodeBuilder) resolveDenoCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/deno",
- "/opt/homebrew/bin/deno",
- }
- }
-
- command := ax.ResolveCommand("deno", paths...)
- if !command.OK {
- return core.Fail(core.E("NodeBuilder.resolveDenoCli", "deno CLI not found. Install it from https://deno.com/runtime", core.NewError(command.Error())))
- }
-
- return command
-}
-
-func (b *NodeBuilder) resolveNpmCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/npm",
- "/opt/homebrew/bin/npm",
- }
- }
-
- command := ax.ResolveCommand("npm", paths...)
- if !command.OK {
- return core.Fail(core.E("NodeBuilder.resolveNpmCli", "npm CLI not found. Install Node.js from https://nodejs.org/", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// findArtifactsForTarget searches for build outputs in the target-specific output directory.
-//
-// artifacts := b.findArtifactsForTarget(storage.Local, "dist", build.Target{OS: "linux", Arch: "amd64"})
-func (b *NodeBuilder) findArtifactsForTarget(fs storage.Medium, outputDir string, target build.Target) []build.Artifact {
- var artifacts []build.Artifact
-
- platformDir := ax.Join(outputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
- if fs.IsDir(platformDir) {
- entriesResult := fs.List(platformDir)
- if entriesResult.OK {
- entries := entriesResult.Value.([]stdfs.DirEntry)
- for _, entry := range entries {
- if entry.IsDir() {
- if target.OS == "darwin" && core.HasSuffix(entry.Name(), ".app") {
- artifacts = append(artifacts, build.Artifact{
- Path: ax.Join(platformDir, entry.Name()),
- OS: target.OS,
- Arch: target.Arch,
- })
- }
- continue
- }
-
- name := entry.Name()
- if core.HasPrefix(name, ".") || name == "CHECKSUMS.txt" {
- continue
- }
-
- artifacts = append(artifacts, build.Artifact{
- Path: ax.Join(platformDir, name),
- OS: target.OS,
- Arch: target.Arch,
- })
- }
- }
- if len(artifacts) > 0 {
- return artifacts
- }
- }
-
- patterns := []string{
- core.Sprintf("*-%s-%s*", target.OS, target.Arch),
- core.Sprintf("*_%s_%s*", target.OS, target.Arch),
- core.Sprintf("*-%s*", target.Arch),
- }
-
- for _, pattern := range patterns {
- entriesResult := fs.List(outputDir)
- if !entriesResult.OK {
- continue
- }
- entries := entriesResult.Value.([]stdfs.DirEntry)
- for _, entry := range entries {
- match := entry.Name()
- matched := core.PathMatch(pattern, match)
- if !matched.OK || !matched.Value.(bool) {
- continue
- }
- fullPath := ax.Join(outputDir, match)
- if fs.IsDir(fullPath) {
- continue
- }
-
- artifacts = append(artifacts, build.Artifact{
- Path: fullPath,
- OS: target.OS,
- Arch: target.Arch,
- })
- }
- if len(artifacts) > 0 {
- break
- }
- }
-
- return artifacts
-}
-
-// Ensure NodeBuilder implements the Builder interface.
-var _ build.Builder = (*NodeBuilder)(nil)
diff --git a/pkg/build/builders/node_example_test.go b/pkg/build/builders/node_example_test.go
deleted file mode 100644
index b15d982..0000000
--- a/pkg/build/builders/node_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewNodeBuilder references NewNodeBuilder on this package API surface.
-func ExampleNewNodeBuilder() {
- _ = NewNodeBuilder
- core.Println("NewNodeBuilder")
- // Output: NewNodeBuilder
-}
-
-// ExampleNodeBuilder_Name references NodeBuilder.Name on this package API surface.
-func ExampleNodeBuilder_Name() {
- _ = (*NodeBuilder).Name
- core.Println("NodeBuilder.Name")
- // Output: NodeBuilder.Name
-}
-
-// ExampleNodeBuilder_Detect references NodeBuilder.Detect on this package API surface.
-func ExampleNodeBuilder_Detect() {
- _ = (*NodeBuilder).Detect
- core.Println("NodeBuilder.Detect")
- // Output: NodeBuilder.Detect
-}
-
-// ExampleNodeBuilder_Build references NodeBuilder.Build on this package API surface.
-func ExampleNodeBuilder_Build() {
- _ = (*NodeBuilder).Build
- core.Println("NodeBuilder.Build")
- // Output: NodeBuilder.Build
-}
diff --git a/pkg/build/builders/node_test.go b/pkg/build/builders/node_test.go
deleted file mode 100644
index 47be65b..0000000
--- a/pkg/build/builders/node_test.go
+++ /dev/null
@@ -1,817 +0,0 @@
-package builders
-
-import (
- "context"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func setupFakeNodeToolchain(t *testing.T, binDir string) {
- t.Helper()
-
- script := `#!/bin/sh
-set -eu
-
-log_file="${NODE_BUILD_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$(basename "$0")" >> "$log_file"
- printf '%s\n' "$@" >> "$log_file"
- printf '%s\n' "GOOS=${GOOS:-}" >> "$log_file"
- printf '%s\n' "GOARCH=${GOARCH:-}" >> "$log_file"
- printf '%s\n' "OUTPUT_DIR=${OUTPUT_DIR:-}" >> "$log_file"
- printf '%s\n' "TARGET_DIR=${TARGET_DIR:-}" >> "$log_file"
- env | sort >> "$log_file"
-fi
-
-output_dir="${OUTPUT_DIR:-dist}"
-platform_dir="${TARGET_DIR:-$output_dir/${GOOS:-}_${GOARCH:-}}"
-mkdir -p "$platform_dir"
-
-name="${NAME:-nodeapp}"
-printf 'fake node artifact\n' > "$platform_dir/$name"
-chmod +x "$platform_dir/$name"
-`
-
- for _, name := range []string{"npm", "pnpm", "yarn", "bun", "deno"} {
- result := ax.WriteFile(ax.Join(binDir, name), []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- }
-}
-
-func setupFakeNodeCommand(t *testing.T, binDir, name string) {
- t.Helper()
-
- script := `#!/bin/sh
-set -eu
-
-log_file="${NODE_BUILD_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$(basename "$0")" >> "$log_file"
- printf '%s\n' "$@" >> "$log_file"
-fi
-
-output_dir="${OUTPUT_DIR:-dist}"
-platform_dir="${TARGET_DIR:-$output_dir/${GOOS:-}_${GOARCH:-}}"
-mkdir -p "$platform_dir"
-printf 'fake node artifact\n' > "$platform_dir/${NAME:-nodeapp}"
-chmod +x "$platform_dir/${NAME:-nodeapp}"
-`
- result := ax.WriteFile(ax.Join(binDir, name), []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func assertNodeLogPrefix(t *testing.T, logPath string, want ...string) []string {
- t.Helper()
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- lines := core.Split(core.Trim(string(content)), "\n")
- if len(lines) < len(want) {
- t.Fatalf("expected %v to be greater than or equal to %v", len(lines), len(want))
- }
- for i, value := range want {
- if !stdlibAssertEqual(value, lines[i]) {
- t.Fatalf("want %v, got %v", value, lines[i])
- }
- }
- return lines
-}
-
-func setupNodeTestProject(t *testing.T) string {
- t.Helper()
-
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "package.json"), []byte(`{"name":"testapp","scripts":{"build":"node build.js"}}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(dir, "build.js"), []byte(`console.log("build")`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return dir
-}
-
-func TestNode_NodeBuilderNameGood(t *testing.T) {
- builder := NewNodeBuilder()
- if !stdlibAssertEqual("node", builder.Name()) {
- t.Fatalf("want %v, got %v", "node", builder.Name())
- }
-
-}
-
-func TestNode_NodeBuilderDetectGood(t *testing.T) {
- fs := storage.Local
-
- t.Run("detects package.json projects", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewNodeBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for empty directory", func(t *testing.T) {
- builder := NewNodeBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, t.TempDir()))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("detects nested package.json projects", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "apps", "web")
- result := ax.MkdirAll(nested, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(nested, "package.json"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewNodeBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects root deno projects", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "deno.json"), []byte(`{"tasks":{"build":"deno eval ''"}}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewNodeBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-}
-
-func TestNode_NodeBuilderBuildGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeNodeToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupNodeTestProject(t)
- outputDir := t.TempDir()
- logDir := t.TempDir()
- logPath := ax.Join(logDir, "node.log")
- t.Setenv("NODE_BUILD_LOG_FILE", logPath)
- result := ax.WriteFile(ax.Join(projectDir, "pnpm-lock.yaml"), []byte("lockfile"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewNodeBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testapp",
- Version: "v1.2.3",
- Env: []string{"FOO=bar"},
- }
-
- targets := []build.Target{
- {OS: "linux", Arch: "amd64"},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
- if !stdlibAssertEqual("linux", artifacts[0].OS) {
- t.Fatalf("want %v, got %v", "linux", artifacts[0].OS)
- }
- if !stdlibAssertEqual("amd64", artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", "amd64", artifacts[0].Arch)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- lines := core.Split(core.Trim(string(content)), "\n")
- if len(lines) < 5 {
- t.Fatalf("expected %v to be greater than or equal to %v", len(lines), 5)
- }
- if !stdlibAssertEqual("pnpm", lines[0]) {
- t.Fatalf("want %v, got %v", "pnpm", lines[0])
- }
- if !stdlibAssertEqual("run", lines[1]) {
- t.Fatalf("want %v, got %v", "run", lines[1])
- }
- if !stdlibAssertEqual("build", lines[2]) {
- t.Fatalf("want %v, got %v", "build", lines[2])
- }
- if !stdlibAssertEqual("GOOS=linux", lines[3]) {
- t.Fatalf("want %v, got %v", "GOOS=linux", lines[3])
- }
- if !stdlibAssertEqual("GOARCH=amd64", lines[4]) {
- t.Fatalf("want %v, got %v", "GOARCH=amd64", lines[4])
- }
- if !stdlibAssertContains(lines, "OUTPUT_DIR="+outputDir) {
- t.Fatalf("expected %v to contain %v", lines, "OUTPUT_DIR="+outputDir)
- }
- if !stdlibAssertContains(lines, "TARGET_DIR="+ax.Join(outputDir, "linux_amd64")) {
- t.Fatalf("expected %v to contain %v", lines, "TARGET_DIR="+ax.Join(outputDir, "linux_amd64"))
- }
- if !stdlibAssertContains(string(content), "FOO=bar") {
- t.Fatalf("expected %v to contain %v", string(content), "FOO=bar")
- }
-
-}
-
-func TestNode_NodeBuilderBuild_Good_Deno(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeNodeToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, "deno.json"), []byte(`{"tasks":{"build":"deno eval ''"}}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "deno.log")
- t.Setenv("NODE_BUILD_LOG_FILE", logPath)
-
- builder := NewNodeBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "denoapp",
- Version: "v1.2.3",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- assertNodeLogPrefix(t, logPath, "deno", "task", "build")
-
-}
-
-func TestNode_NodeBuilderBuild_Good_DenoOverrideFromConfig(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeNodeToolchain(t, binDir)
- setupFakeNodeCommand(t, binDir, "deno-build")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, "deno.json"), []byte(`{"tasks":{"build":"deno eval ''"}}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "deno-override.log")
- t.Setenv("NODE_BUILD_LOG_FILE", logPath)
-
- builder := NewNodeBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "denoapp",
- DenoBuild: "deno-build --target release",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- assertNodeLogPrefix(t, logPath, "deno-build", "--target", "release")
-
-}
-
-func TestNode_NodeBuilderBuild_Good_DenoOverrideFromEnvWins(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeNodeToolchain(t, binDir)
- setupFakeNodeCommand(t, binDir, "deno-build")
- setupFakeNodeCommand(t, binDir, "env-deno-build")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
- t.Setenv("DENO_BUILD", "env-deno-build --env")
-
- projectDir := t.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, "deno.json"), []byte(`{"tasks":{"build":"deno eval ''"}}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "deno-env-override.log")
- t.Setenv("NODE_BUILD_LOG_FILE", logPath)
-
- builder := NewNodeBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "denoapp",
- DenoBuild: "deno-build --config",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- assertNodeLogPrefix(t, logPath, "env-deno-build", "--env")
-
-}
-
-func TestNode_NodeBuilderBuild_Good_NpmOverrideFromConfig(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeNodeToolchain(t, binDir)
- setupFakeNodeCommand(t, binDir, "npm-build")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, "package.json"), []byte(`{"name":"testapp","scripts":{"build":"node build.js"}}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "npm-override.log")
- t.Setenv("NODE_BUILD_LOG_FILE", logPath)
-
- builder := NewNodeBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "npmapp",
- NpmBuild: "npm-build --scope app",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- assertNodeLogPrefix(t, logPath, "npm-build", "--scope", "app")
-
-}
-
-func TestNode_NodeBuilderBuild_Good_DenoEnableWithoutManifest(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeNodeToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
- t.Setenv("DENO_ENABLE", "true")
-
- projectDir := t.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "deno-enable.log")
- t.Setenv("NODE_BUILD_LOG_FILE", logPath)
-
- builder := NewNodeBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "denoapp",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- assertNodeLogPrefix(t, logPath, "deno", "task", "build")
-
-}
-
-func TestNode_NodeBuilderBuild_Good_DenoOverrideWithoutManifest(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeNodeToolchain(t, binDir)
- setupFakeNodeCommand(t, binDir, "deno-build")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "deno-config.log")
- t.Setenv("NODE_BUILD_LOG_FILE", logPath)
-
- builder := NewNodeBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "denoapp",
- DenoBuild: "deno-build --target release",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- assertNodeLogPrefix(t, logPath, "deno-build", "--target", "release")
-
-}
-
-func TestNode_ResolvePackageManagerGood(t *testing.T) {
- fs := storage.Local
- builder := NewNodeBuilder()
-
- t.Run("prefers packageManager declaration over lockfiles", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "package.json"), []byte(`{"packageManager":"pnpm@9.12.0"}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(dir, "bun.lockb"), []byte(""), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- manager := requireCPPString(t, builder.resolvePackageManager(fs, dir))
- if !stdlibAssertEqual("pnpm", manager) {
- t.Fatalf("want %v, got %v", "pnpm", manager)
- }
-
- })
-
- t.Run("normalises package manager version pins", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "package.json"), []byte(`{"packageManager":"bun@1.1.38"}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- manager := requireCPPString(t, builder.resolvePackageManager(fs, dir))
- if !stdlibAssertEqual("bun", manager) {
- t.Fatalf("want %v, got %v", "bun", manager)
- }
-
- })
-}
-
-func TestNode_NodeBuilderFindArtifactsForTargetGood(t *testing.T) {
- fs := storage.Local
- builder := NewNodeBuilder()
-
- t.Run("finds files in platform subdirectory", func(t *testing.T) {
- dir := t.TempDir()
- platformDir := ax.Join(dir, "linux_amd64")
- result := ax.MkdirAll(platformDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- artifactPath := ax.Join(platformDir, "testapp")
- result = ax.WriteFile(artifactPath, []byte("binary"), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- artifacts := builder.findArtifactsForTarget(fs, dir, build.Target{OS: "linux", Arch: "amd64"})
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(artifactPath, artifacts[0].Path) {
- t.Fatalf("want %v, got %v", artifactPath, artifacts[0].Path)
- }
-
- })
-
- t.Run("finds darwin app bundles", func(t *testing.T) {
- dir := t.TempDir()
- platformDir := ax.Join(dir, "darwin_arm64")
- appDir := ax.Join(platformDir, "TestApp.app")
- result := ax.MkdirAll(appDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- artifacts := builder.findArtifactsForTarget(fs, dir, build.Target{OS: "darwin", Arch: "arm64"})
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(appDir, artifacts[0].Path) {
- t.Fatalf("want %v, got %v", appDir, artifacts[0].Path)
- }
-
- })
-
- t.Run("falls back to name patterns in root", func(t *testing.T) {
- dir := t.TempDir()
- artifactPath := ax.Join(dir, "testapp-linux-amd64")
- result := ax.WriteFile(artifactPath, []byte("binary"), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- artifacts := builder.findArtifactsForTarget(fs, dir, build.Target{OS: "linux", Arch: "amd64"})
- if stdlibAssertEmpty(artifacts) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertEqual(artifactPath, artifacts[0].Path) {
- t.Fatalf("want %v, got %v", artifactPath, artifacts[0].Path)
- }
-
- })
-}
-
-func TestNode_NodeBuilderInterfaceGood(t *testing.T) {
- builder := NewNodeBuilder()
- var _ build.Builder = builder
- if !stdlibAssertEqual("node", builder.Name()) {
- t.Fatalf("want %v, got %v", "node", builder.Name())
- }
- detected := requireCPPBool(t, builder.Detect(nil, t.TempDir()))
- if detected {
- t.Fatal("expected empty temp directory not to be detected")
- }
-}
-
-func TestNode_NodeBuilderBuildDefaultsGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeNodeToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupNodeTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewNodeBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Env: []string{"FOO=bar"},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, nil))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(runtime.GOOS, artifacts[0].OS) {
- t.Fatalf("want %v, got %v", runtime.GOOS, artifacts[0].OS)
- }
- if !stdlibAssertEqual(runtime.GOARCH, artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", runtime.GOARCH, artifacts[0].Arch)
- }
-
-}
-
-func TestNode_NodeBuilderBuild_Good_NestedProject(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeNodeToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := t.TempDir()
- nestedDir := ax.Join(projectDir, "apps", "web")
- result := ax.MkdirAll(nestedDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(nestedDir, "package.json"), []byte(`{"name":"nested-app","scripts":{"build":"node build.js"}}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(nestedDir, "build.js"), []byte(`console.log("nested build")`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- logDir := t.TempDir()
- logPath := ax.Join(logDir, "node-nested.log")
- t.Setenv("NODE_BUILD_LOG_FILE", logPath)
-
- builder := NewNodeBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "nested-app",
- Version: "v1.2.3",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "apps/web") {
- t.Fatalf("expected %v to contain %v", string(content), "apps/web")
- }
- if !stdlibAssertContains(string(content), "GOOS=linux") {
- t.Fatalf("expected %v to contain %v", string(content), "GOOS=linux")
- }
- if !stdlibAssertContains(string(content), "GOARCH=amd64") {
- t.Fatalf("expected %v to contain %v", string(content), "GOARCH=amd64")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestNode_NewNodeBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewNodeBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestNode_NewNodeBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewNodeBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestNode_NewNodeBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewNodeBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestNode_NodeBuilder_Name_Good(t *core.T) {
- subject := &NodeBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestNode_NodeBuilder_Name_Bad(t *core.T) {
- subject := &NodeBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestNode_NodeBuilder_Name_Ugly(t *core.T) {
- subject := &NodeBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestNode_NodeBuilder_Detect_Good(t *core.T) {
- subject := &NodeBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestNode_NodeBuilder_Detect_Bad(t *core.T) {
- subject := &NodeBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestNode_NodeBuilder_Detect_Ugly(t *core.T) {
- subject := &NodeBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestNode_NodeBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &NodeBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestNode_NodeBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &NodeBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestNode_NodeBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &NodeBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/package_manager.go b/pkg/build/builders/package_manager.go
deleted file mode 100644
index ff61060..0000000
--- a/pkg/build/builders/package_manager.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package builders
-
-import (
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-type packageJSONManifest struct {
- PackageManager string `json:"packageManager"`
-}
-
-// detectDeclaredPackageManager reads package.json and returns the declared package manager.
-//
-// manager := detectDeclaredPackageManager(storage.Local, ".")
-func detectDeclaredPackageManager(fs storage.Medium, dir string) string {
- contentResult := fs.Read(ax.Join(dir, "package.json"))
- if !contentResult.OK {
- return ""
- }
- content := contentResult.Value.(string)
-
- var manifest packageJSONManifest
- decoded := ax.JSONUnmarshal([]byte(content), &manifest)
- if !decoded.OK {
- return ""
- }
-
- return normalisePackageManager(manifest.PackageManager)
-}
-
-// normalisePackageManager trims any pinned version from a packageManager declaration.
-//
-// manager := normalisePackageManager("pnpm@9.12.0")
-func normalisePackageManager(value string) string {
- value = core.Trim(value)
- if value == "" {
- return ""
- }
-
- parts := core.SplitN(value, "@", 2)
- manager := parts[0]
-
- switch manager {
- case "bun", "pnpm", "yarn", "npm":
- return manager
- default:
- return ""
- }
-}
diff --git a/pkg/build/builders/php.go b/pkg/build/builders/php.go
deleted file mode 100644
index 0e95122..0000000
--- a/pkg/build/builders/php.go
+++ /dev/null
@@ -1,205 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// PHPBuilder builds PHP projects with composer.json manifests.
-//
-// b := builders.NewPHPBuilder()
-type PHPBuilder struct{}
-
-// NewPHPBuilder creates a new PHP builder instance.
-//
-// b := builders.NewPHPBuilder()
-func NewPHPBuilder() *PHPBuilder {
- return &PHPBuilder{}
-}
-
-// Name returns the builder's identifier.
-//
-// name := b.Name() // → "php"
-func (b *PHPBuilder) Name() string {
- return "php"
-}
-
-// Detect checks if this builder can handle the project in the given directory.
-//
-// ok, err := b.Detect(storage.Local, ".")
-func (b *PHPBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(build.IsPHPProject(fs, dir))
-}
-
-// Build installs dependencies and produces either composer-generated artifacts
-// or a deterministic bundle when the project does not emit build outputs.
-//
-// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
-func (b *PHPBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("PHPBuilder.Build", "config is nil", nil))
- }
- filesystem := ensureBuildFilesystem(cfg)
-
- targets = defaultRuntimeTargets(targets, runtime.GOOS, runtime.GOARCH)
-
- outputDir := cfg.OutputDir
- if outputDir == "" {
- outputDir = defaultOutputDir(cfg)
- }
- created := ensureOutputDir(filesystem, outputDir, "PHPBuilder.Build")
- if !created.OK {
- return created
- }
-
- composerCommandResult := b.resolveComposerCli()
- if !composerCommandResult.OK {
- return composerCommandResult
- }
- composerCommand := composerCommandResult.Value.(string)
-
- installed := b.installDependencies(ctx, cfg, composerCommand)
- if !installed.OK {
- return installed
- }
-
- hasBuildScriptResult := b.hasBuildScript(cfg.FS, cfg.ProjectDir)
- if !hasBuildScriptResult.OK {
- return hasBuildScriptResult
- }
- hasBuildScript := hasBuildScriptResult.Value.(bool)
-
- var artifacts []build.Artifact
- for _, target := range targets {
- platformDirResult := ensurePlatformDir(filesystem, outputDir, target, "PHPBuilder.Build")
- if !platformDirResult.OK {
- return platformDirResult
- }
- platformDir := platformDirResult.Value.(string)
-
- env := configuredTargetEnv(cfg, target, standardTargetValues(outputDir, platformDir, target)...)
-
- if hasBuildScript {
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, env, composerCommand, "run-script", "build")
- if !output.OK {
- return core.Fail(core.E("PHPBuilder.Build", "composer build failed: "+output.Error(), core.NewError(output.Error())))
- }
- }
-
- found := (&NodeBuilder{}).findArtifactsForTarget(filesystem, outputDir, target)
- if len(found) == 0 {
- bundlePath := ax.Join(platformDir, b.bundleName(cfg)+".zip")
- bundled := b.bundleProject(filesystem, cfg.ProjectDir, outputDir, bundlePath)
- if !bundled.OK {
- return bundled
- }
-
- found = append(found, build.Artifact{
- Path: bundlePath,
- OS: target.OS,
- Arch: target.Arch,
- })
- }
-
- artifacts = append(artifacts, found...)
- }
-
- return core.Ok(artifacts)
-}
-
-// installDependencies runs composer install once before the per-target build.
-func (b *PHPBuilder) installDependencies(ctx context.Context, cfg *build.Config, composerCommand string) core.Result {
- args := []string{"install", "--no-interaction", "--no-dev", "--prefer-dist", "--optimize-autoloader"}
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, build.BuildEnvironment(cfg), composerCommand, args...)
- if !output.OK {
- return core.Fail(core.E("PHPBuilder.installDependencies", "composer install failed: "+output.Error(), core.NewError(output.Error())))
- }
- return core.Ok(nil)
-}
-
-// hasBuildScript reports whether composer.json defines a build script.
-func (b *PHPBuilder) hasBuildScript(fs storage.Medium, projectDir string) core.Result {
- content := fs.Read(ax.Join(projectDir, "composer.json"))
- if !content.OK {
- return core.Fail(core.E("PHPBuilder.hasBuildScript", "failed to read composer.json", core.NewError(content.Error())))
- }
-
- var manifest struct {
- Scripts map[string]any `json:"scripts"`
- }
- decoded := ax.JSONUnmarshal([]byte(content.Value.(string)), &manifest)
- if !decoded.OK {
- return core.Fail(core.E("PHPBuilder.hasBuildScript", "failed to parse composer.json", core.NewError(decoded.Error())))
- }
-
- _, ok := manifest.Scripts["build"]
- return core.Ok(ok)
-}
-
-// bundleName returns the bundle filename stem.
-func (b *PHPBuilder) bundleName(cfg *build.Config) string {
- if cfg.Name != "" {
- return cfg.Name
- }
- if cfg.ProjectDir != "" {
- return ax.Base(cfg.ProjectDir)
- }
- return "php-app"
-}
-
-// bundleProject creates a zip bundle containing the project tree.
-func (b *PHPBuilder) bundleProject(fs storage.Medium, projectDir, outputDir, bundlePath string) core.Result {
- exclude := func(path string) bool {
- return b.isExcludedPath(path, outputDir, bundlePath)
- }
- return bundleZipTree(fs, projectDir, bundlePath, "PHPBuilder.bundleProject", exclude)
-}
-
-// isExcludedPath reports whether a path should be omitted from the bundle.
-func (b *PHPBuilder) isExcludedPath(path, outputDir, bundlePath string) bool {
- cleanPath := ax.Clean(path)
- cleanOutputDir := ax.Clean(outputDir)
- cleanBundlePath := ax.Clean(bundlePath)
-
- if cleanPath == cleanOutputDir || core.HasPrefix(cleanPath, cleanOutputDir+ax.DS()) {
- return true
- }
- if cleanPath == cleanBundlePath {
- return true
- }
-
- base := ax.Base(cleanPath)
- switch base {
- case ".git", ".core":
- return true
- default:
- return false
- }
-}
-
-// resolveComposerCli returns the executable path for the composer CLI.
-func (b *PHPBuilder) resolveComposerCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/composer",
- "/opt/homebrew/bin/composer",
- "/usr/bin/composer",
- }
- }
-
- command := ax.ResolveCommand("composer", paths...)
- if !command.OK {
- return core.Fail(core.E("PHPBuilder.resolveComposerCli", "composer CLI not found. Install it from https://getcomposer.org/", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// Ensure PHPBuilder implements the Builder interface.
-var _ build.Builder = (*PHPBuilder)(nil)
diff --git a/pkg/build/builders/php_example_test.go b/pkg/build/builders/php_example_test.go
deleted file mode 100644
index b7cbcdf..0000000
--- a/pkg/build/builders/php_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewPHPBuilder references NewPHPBuilder on this package API surface.
-func ExampleNewPHPBuilder() {
- _ = NewPHPBuilder
- core.Println("NewPHPBuilder")
- // Output: NewPHPBuilder
-}
-
-// ExamplePHPBuilder_Name references PHPBuilder.Name on this package API surface.
-func ExamplePHPBuilder_Name() {
- _ = (*PHPBuilder).Name
- core.Println("PHPBuilder.Name")
- // Output: PHPBuilder.Name
-}
-
-// ExamplePHPBuilder_Detect references PHPBuilder.Detect on this package API surface.
-func ExamplePHPBuilder_Detect() {
- _ = (*PHPBuilder).Detect
- core.Println("PHPBuilder.Detect")
- // Output: PHPBuilder.Detect
-}
-
-// ExamplePHPBuilder_Build references PHPBuilder.Build on this package API surface.
-func ExamplePHPBuilder_Build() {
- _ = (*PHPBuilder).Build
- core.Println("PHPBuilder.Build")
- // Output: PHPBuilder.Build
-}
diff --git a/pkg/build/builders/php_test.go b/pkg/build/builders/php_test.go
deleted file mode 100644
index 86b02e7..0000000
--- a/pkg/build/builders/php_test.go
+++ /dev/null
@@ -1,408 +0,0 @@
-package builders
-
-import (
- "archive/zip"
- "context"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func setupFakePHPToolchain(t *testing.T, binDir string) {
- t.Helper()
-
- script := `#!/bin/sh
-set -eu
-
-log_file="${PHP_BUILD_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$(basename "$0")" >> "$log_file"
- printf '%s\n' "$@" >> "$log_file"
- printf '%s\n' "GOOS=${GOOS:-}" >> "$log_file"
- printf '%s\n' "GOARCH=${GOARCH:-}" >> "$log_file"
- printf '%s\n' "OUTPUT_DIR=${OUTPUT_DIR:-}" >> "$log_file"
- printf '%s\n' "TARGET_DIR=${TARGET_DIR:-}" >> "$log_file"
- env | sort >> "$log_file"
-fi
-
-output_dir="${OUTPUT_DIR:-dist}"
-platform_dir="${TARGET_DIR:-$output_dir/${GOOS:-}_${GOARCH:-}}"
-mkdir -p "$platform_dir"
-
-if [ "${1:-}" = "run-script" ] && [ "${2:-}" = "build" ]; then
- artifact="${platform_dir}/${NAME:-phpapp}"
- printf 'fake php artifact\n' > "$artifact"
- chmod +x "$artifact"
-fi
-`
- result := ax.WriteFile(ax.Join(binDir, "composer"), []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func setupPHPTestProject(t *testing.T, withBuildScript bool) string {
- t.Helper()
-
- dir := t.TempDir()
-
- composerJSON := `{"name":"test/php-app"}`
- if withBuildScript {
- composerJSON = `{"name":"test/php-app","scripts":{"build":"php build.php"}}`
- }
- result := ax.WriteFile(ax.Join(dir, "composer.json"), []byte(composerJSON), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(dir, "index.php"), []byte("> "$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"
- env | sort >> "$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
-`
- result := ax.WriteFile(ax.Join(binDir, "cargo"), []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func setupRustTestProject(t *testing.T) string {
- t.Helper()
-
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "Cargo.toml"), []byte("[package]\nname = \"testapp\"\nversion = \"0.1.0\""), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.MkdirAll(ax.Join(dir, "src"), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(dir, "src", "main.rs"), []byte("fn main() {}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return dir
-}
-
-func TestRust_RustBuilderNameGood(t *testing.T) {
- builder := NewRustBuilder()
- if !stdlibAssertEqual("rust", builder.Name()) {
- t.Fatalf("want %v, got %v", "rust", builder.Name())
- }
-
-}
-
-func TestRust_RustBuilderDetectGood(t *testing.T) {
- fs := storage.Local
-
- t.Run("detects Cargo.toml projects", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "Cargo.toml"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewRustBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for empty directory", func(t *testing.T) {
- builder := NewRustBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, t.TempDir()))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestRust_RustBuilderBuildGood(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(core.PathListSeparator)+core.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: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testapp",
- Version: "v1.2.3",
- Env: []string{"FOO=bar"},
- }
-
- targets := []build.Target{{OS: "linux", Arch: "amd64"}}
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
- if !stdlibAssertEqual("linux", artifacts[0].OS) {
- t.Fatalf("want %v, got %v", "linux", artifacts[0].OS)
- }
- if !stdlibAssertEqual("amd64", artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", "amd64", artifacts[0].Arch)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- lines := core.Split(core.Trim(string(content)), "\n")
- if len(lines) < 5 {
- t.Fatalf("expected %v to be greater than or equal to %v", len(lines), 5)
- }
- if !stdlibAssertEqual("cargo", lines[0]) {
- t.Fatalf("want %v, got %v", "cargo", lines[0])
- }
- if !stdlibAssertEqual("build", lines[1]) {
- t.Fatalf("want %v, got %v", "build", lines[1])
- }
- if !stdlibAssertEqual("--release", lines[2]) {
- t.Fatalf("want %v, got %v", "--release", lines[2])
- }
- if !stdlibAssertEqual("--target", lines[3]) {
- t.Fatalf("want %v, got %v", "--target", lines[3])
- }
- if !stdlibAssertEqual("x86_64-unknown-linux-gnu", lines[4]) {
- t.Fatalf("want %v, got %v", "x86_64-unknown-linux-gnu", lines[4])
- }
- if !stdlibAssertContains(lines, "CARGO_TARGET_DIR="+ax.Join(outputDir, "linux_amd64")) {
- t.Fatalf("expected %v to contain %v", lines, "CARGO_TARGET_DIR="+ax.Join(outputDir, "linux_amd64"))
- }
- if !stdlibAssertContains(string(content), "FOO=bar") {
- t.Fatalf("expected %v to contain %v", string(content), "FOO=bar")
- }
-
-}
-
-func TestRust_RustBuilderInterfaceGood(t *testing.T) {
- builder := NewRustBuilder()
- var _ build.Builder = builder
- if !stdlibAssertEqual("rust", builder.Name()) {
- t.Fatalf("want %v, got %v", "rust", builder.Name())
- }
- detected := requireCPPBool(t, builder.Detect(nil, t.TempDir()))
- if detected {
- t.Fatal("expected empty temp directory not to be detected")
- }
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestRust_NewRustBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewRustBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRust_NewRustBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewRustBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRust_NewRustBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewRustBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRust_RustBuilder_Name_Good(t *core.T) {
- subject := &RustBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRust_RustBuilder_Name_Bad(t *core.T) {
- subject := &RustBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRust_RustBuilder_Name_Ugly(t *core.T) {
- subject := &RustBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRust_RustBuilder_Detect_Good(t *core.T) {
- subject := &RustBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRust_RustBuilder_Detect_Bad(t *core.T) {
- subject := &RustBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRust_RustBuilder_Detect_Ugly(t *core.T) {
- subject := &RustBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRust_RustBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &RustBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRust_RustBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &RustBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRust_RustBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &RustBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/taskfile.go b/pkg/build/builders/taskfile.go
deleted file mode 100644
index 09eef98..0000000
--- a/pkg/build/builders/taskfile.go
+++ /dev/null
@@ -1,313 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- stdfs "io/fs"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// TaskfileBuilder builds projects using Taskfile (https://taskfile.dev/).
-// This is a generic builder that can handle any project type that has a Taskfile.
-//
-// b := builders.NewTaskfileBuilder()
-type TaskfileBuilder struct{}
-
-// NewTaskfileBuilder creates a new Taskfile builder.
-//
-// b := builders.NewTaskfileBuilder()
-func NewTaskfileBuilder() *TaskfileBuilder {
- return &TaskfileBuilder{}
-}
-
-// Name returns the builder's identifier.
-//
-// name := b.Name() // → "taskfile"
-func (b *TaskfileBuilder) Name() string {
- return "taskfile"
-}
-
-// Detect checks if a Taskfile exists in the directory.
-//
-// ok, err := b.Detect(storage.Local, ".")
-func (b *TaskfileBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(build.IsTaskfileProject(fs, dir))
-}
-
-// Build runs the Taskfile build task for each target platform.
-//
-// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
-func (b *TaskfileBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("TaskfileBuilder.Build", "config is nil", nil))
- }
- filesystem := ensureBuildFilesystem(cfg)
-
- taskCommandResult := b.resolveTaskCli()
- if !taskCommandResult.OK {
- return taskCommandResult
- }
- taskCommand := taskCommandResult.Value.(string)
-
- // Create output directory
- outputDir := cfg.OutputDir
- if outputDir == "" {
- outputDir = defaultOutputDir(cfg)
- }
- created := ensureOutputDir(filesystem, outputDir, "TaskfileBuilder.Build")
- if !created.OK {
- return created
- }
-
- var artifacts []build.Artifact
-
- // If no targets are specified, build the host target so Taskfile builds
- // still receive the standard GOOS/GOARCH surface.
- targets = defaultRuntimeTargets(targets, runtime.GOOS, runtime.GOARCH)
-
- // Run build task for each target
- for _, target := range targets {
- ran := b.runTask(ctx, cfg, taskCommand, outputDir, target)
- if !ran.OK {
- return ran
- }
-
- // Try to find artifacts for this target
- found := b.findArtifactsForTarget(cfg.FS, outputDir, target)
- artifacts = append(artifacts, found...)
- }
-
- return core.Ok(artifacts)
-}
-
-// runTask executes the Taskfile build task.
-func (b *TaskfileBuilder) runTask(ctx context.Context, cfg *build.Config, taskCommand string, outputDir string, target build.Target) core.Result {
- // Build task command
- args := []string{"build"}
- env := build.BuildEnvironment(cfg)
- targetDir := platformDir(outputDir, target)
- values := standardTargetValues(outputDir, targetDir, target)
- if cfg.Name != "" {
- values = append(values, core.Sprintf("NAME=%s", cfg.Name))
- }
- if cfg.Version != "" {
- values = append(values, core.Sprintf("VERSION=%s", cfg.Version))
- }
- values = append(values, cgoEnvValue(cfg.CGO))
- args = append(args, values...)
- env = append(env, values...)
-
- cleanup := func() {}
- if cfg != nil {
- surfaceResult := b.applyWailsV3BuildSurface(cfg, target, args, env)
- if !surfaceResult.OK {
- return surfaceResult
- }
- surface := surfaceResult.Value.(taskBuildSurface)
- args = surface.args
- env = surface.env
- cleanup = surface.cleanup
- }
- defer cleanup()
-
- if target.OS != "" && target.Arch != "" {
- core.Print(nil, "Running task build for %s/%s", target.OS, target.Arch)
- } else {
- core.Print(nil, "Running task build")
- }
-
- executed := ax.ExecWithEnv(ctx, cfg.ProjectDir, env, taskCommand, args...)
- if !executed.OK {
- return core.Fail(core.E("TaskfileBuilder.runTask", "task build failed", core.NewError(executed.Error())))
- }
-
- return core.Ok(nil)
-}
-
-type taskBuildSurface struct {
- args []string
- env []string
- cleanup func()
-}
-
-func (b *TaskfileBuilder) applyWailsV3BuildSurface(cfg *build.Config, target build.Target, args, env []string) core.Result {
- if cfg == nil || cfg.ProjectDir == "" {
- return core.Ok(taskBuildSurface{args: args, env: env, cleanup: func() {}})
- }
-
- fs := cfg.FS
- if fs == nil {
- fs = storage.Local
- }
-
- wailsBuilder := NewWailsBuilder()
- if !build.IsWailsProject(fs, cfg.ProjectDir) || !wailsBuilder.isWailsV3(fs, cfg.ProjectDir) {
- return core.Ok(taskBuildSurface{args: args, env: env, cleanup: func() {}})
- }
-
- goflagsResult := buildV3GoFlags(cfg)
- if !goflagsResult.OK {
- return goflagsResult
- }
- if goflags := goflagsResult.Value.(string); goflags != "" {
- env = append(env, "GOFLAGS="+goflags)
- }
-
- taskVarsResult := buildV3TaskVars(cfg, target)
- if !taskVarsResult.OK {
- return taskVarsResult
- }
- taskVars := taskVarsResult.Value.([]string)
- if len(taskVars) > 0 {
- args = append(args, taskVars...)
- env = append(env, taskVars...)
- }
-
- if !cfg.Obfuscate {
- return core.Ok(taskBuildSurface{args: args, env: env, cleanup: func() {}})
- }
-
- obfuscationResult := wailsBuilder.prepareV3Obfuscation(env)
- if !obfuscationResult.OK {
- return obfuscationResult
- }
- obfuscation := obfuscationResult.Value.(obfuscationEnv)
-
- return core.Ok(taskBuildSurface{args: args, env: obfuscation.env, cleanup: obfuscation.cleanup})
-}
-
-// findArtifacts searches for built artifacts in the output directory.
-func (b *TaskfileBuilder) findArtifacts(fs storage.Medium, outputDir string) []build.Artifact {
- var artifacts []build.Artifact
-
- entriesResult := fs.List(outputDir)
- if !entriesResult.OK {
- return artifacts
- }
- entries := entriesResult.Value.([]stdfs.DirEntry)
-
- for _, entry := range entries {
- if entry.IsDir() {
- continue
- }
-
- // Skip common non-artifact files
- name := entry.Name()
- if core.HasPrefix(name, ".") || name == "CHECKSUMS.txt" {
- continue
- }
-
- artifacts = append(artifacts, build.Artifact{
- Path: ax.Join(outputDir, name),
- OS: "",
- Arch: "",
- })
- }
-
- return artifacts
-}
-
-// findArtifactsForTarget searches for built artifacts for a specific target.
-func (b *TaskfileBuilder) findArtifactsForTarget(fs storage.Medium, outputDir string, target build.Target) []build.Artifact {
- var artifacts []build.Artifact
-
- // 1. Look for platform-specific subdirectory: output/os_arch/
- platformSubdir := ax.Join(outputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
- if fs.IsDir(platformSubdir) {
- entriesResult := fs.List(platformSubdir)
- entries := []stdfs.DirEntry{}
- if entriesResult.OK {
- entries = entriesResult.Value.([]stdfs.DirEntry)
- }
- for _, entry := range entries {
- if entry.IsDir() {
- // Handle .app bundles on macOS
- if target.OS == "darwin" && core.HasSuffix(entry.Name(), ".app") {
- artifacts = append(artifacts, build.Artifact{
- Path: ax.Join(platformSubdir, entry.Name()),
- OS: target.OS,
- Arch: target.Arch,
- })
- }
- continue
- }
- // Skip hidden files
- if core.HasPrefix(entry.Name(), ".") {
- continue
- }
- artifacts = append(artifacts, build.Artifact{
- Path: ax.Join(platformSubdir, entry.Name()),
- OS: target.OS,
- Arch: target.Arch,
- })
- }
- if len(artifacts) > 0 {
- return artifacts
- }
- }
-
- // 2. Look for files matching the target pattern in the root output dir
- patterns := []string{
- core.Sprintf("*-%s-%s*", target.OS, target.Arch),
- core.Sprintf("*_%s_%s*", target.OS, target.Arch),
- core.Sprintf("*-%s*", target.Arch),
- }
-
- for _, pattern := range patterns {
- entriesResult := fs.List(outputDir)
- entries := []stdfs.DirEntry{}
- if entriesResult.OK {
- entries = entriesResult.Value.([]stdfs.DirEntry)
- }
- for _, entry := range entries {
- match := entry.Name()
- // Simple glob matching
- if b.matchPattern(match, pattern) {
- fullPath := ax.Join(outputDir, match)
- if fs.IsDir(fullPath) {
- continue
- }
-
- artifacts = append(artifacts, build.Artifact{
- Path: fullPath,
- OS: target.OS,
- Arch: target.Arch,
- })
- }
- }
-
- if len(artifacts) > 0 {
- break // Found matches, stop looking
- }
- }
-
- return artifacts
-}
-
-// matchPattern implements glob matching for Taskfile artifacts.
-func (b *TaskfileBuilder) matchPattern(name, pattern string) bool {
- matched := core.PathMatch(pattern, name)
- return matched.OK && matched.Value.(bool)
-}
-
-// resolveTaskCli returns the executable path for the task CLI.
-func (b *TaskfileBuilder) resolveTaskCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/task",
- "/opt/homebrew/bin/task",
- }
- }
-
- command := ax.ResolveCommand("task", paths...)
- if !command.OK {
- return core.Fail(core.E("TaskfileBuilder.resolveTaskCli", "task CLI not found. Install with: brew install go-task (macOS), go install github.com/go-task/task/v3/cmd/task@latest, or see https://taskfile.dev/installation/", core.NewError(command.Error())))
- }
-
- return command
-}
diff --git a/pkg/build/builders/taskfile_example_test.go b/pkg/build/builders/taskfile_example_test.go
deleted file mode 100644
index 7de3a50..0000000
--- a/pkg/build/builders/taskfile_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewTaskfileBuilder references NewTaskfileBuilder on this package API surface.
-func ExampleNewTaskfileBuilder() {
- _ = NewTaskfileBuilder
- core.Println("NewTaskfileBuilder")
- // Output: NewTaskfileBuilder
-}
-
-// ExampleTaskfileBuilder_Name references TaskfileBuilder.Name on this package API surface.
-func ExampleTaskfileBuilder_Name() {
- _ = (*TaskfileBuilder).Name
- core.Println("TaskfileBuilder.Name")
- // Output: TaskfileBuilder.Name
-}
-
-// ExampleTaskfileBuilder_Detect references TaskfileBuilder.Detect on this package API surface.
-func ExampleTaskfileBuilder_Detect() {
- _ = (*TaskfileBuilder).Detect
- core.Println("TaskfileBuilder.Detect")
- // Output: TaskfileBuilder.Detect
-}
-
-// ExampleTaskfileBuilder_Build references TaskfileBuilder.Build on this package API surface.
-func ExampleTaskfileBuilder_Build() {
- _ = (*TaskfileBuilder).Build
- core.Println("TaskfileBuilder.Build")
- // Output: TaskfileBuilder.Build
-}
diff --git a/pkg/build/builders/taskfile_test.go b/pkg/build/builders/taskfile_test.go
deleted file mode 100644
index 2078c03..0000000
--- a/pkg/build/builders/taskfile_test.go
+++ /dev/null
@@ -1,845 +0,0 @@
-package builders
-
-import (
- "context"
- "runtime"
- "testing"
-
- "dappco.re/go/build/internal/ax"
-
- core "dappco.re/go"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func TestTaskfile_TaskfileBuilderNameGood(t *testing.T) {
- builder := NewTaskfileBuilder()
- if !stdlibAssertEqual("taskfile", builder.Name()) {
- t.Fatalf("want %v, got %v", "taskfile", builder.Name())
- }
-
-}
-
-func TestTaskfile_TaskfileBuilderDetectGood(t *testing.T) {
- fs := storage.Local
-
- t.Run("detects Taskfile.yml", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "Taskfile.yml"), []byte("version: '3'\n"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewTaskfileBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects Taskfile.yaml", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "Taskfile.yaml"), []byte("version: '3'\n"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewTaskfileBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects Taskfile (no extension)", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "Taskfile"), []byte("version: '3'\n"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewTaskfileBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects lowercase taskfile.yml", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "taskfile.yml"), []byte("version: '3'\n"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewTaskfileBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects lowercase taskfile.yaml", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "taskfile.yaml"), []byte("version: '3'\n"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewTaskfileBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for empty directory", func(t *testing.T) {
- dir := t.TempDir()
-
- builder := NewTaskfileBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false for non-Taskfile project", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "Makefile"), []byte("all:\n\techo hello\n"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewTaskfileBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("does not match Taskfile in subdirectory", func(t *testing.T) {
- dir := t.TempDir()
- subDir := ax.Join(dir, "subdir")
- result := ax.MkdirAll(subDir, 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- result = ax.WriteFile(ax.Join(subDir, "Taskfile.yml"), []byte("version: '3'\n"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewTaskfileBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestTaskfile_TaskfileBuilderFindArtifactsGood(t *testing.T) {
- fs := storage.Local
- builder := NewTaskfileBuilder()
-
- t.Run("finds files in output directory", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(dir, "myapp.tar.gz"), []byte("archive"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- artifacts := builder.findArtifacts(fs, dir)
- if len(artifacts) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(artifacts))
- }
-
- })
-
- t.Run("skips hidden files", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(dir, ".hidden"), []byte("hidden"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- artifacts := builder.findArtifacts(fs, dir)
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertContains(artifacts[0].Path, "myapp") {
- t.Fatalf("expected %v to contain %v", artifacts[0].Path, "myapp")
- }
-
- })
-
- t.Run("skips CHECKSUMS.txt", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(dir, "CHECKSUMS.txt"), []byte("sha256"), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- artifacts := builder.findArtifacts(fs, dir)
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertContains(artifacts[0].Path, "myapp") {
- t.Fatalf("expected %v to contain %v", artifacts[0].Path, "myapp")
- }
-
- })
-
- t.Run("skips directories", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.MkdirAll(ax.Join(dir, "subdir"), 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- artifacts := builder.findArtifacts(fs, dir)
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- })
-
- t.Run("returns empty for empty directory", func(t *testing.T) {
- dir := t.TempDir()
-
- artifacts := builder.findArtifacts(fs, dir)
- if !stdlibAssertEmpty(artifacts) {
- t.Fatalf("expected empty, got %v", artifacts)
- }
-
- })
-
- t.Run("returns empty for nonexistent directory", func(t *testing.T) {
- artifacts := builder.findArtifacts(fs, "/nonexistent/path")
- if !stdlibAssertEmpty(artifacts) {
- t.Fatalf("expected empty, got %v", artifacts)
- }
-
- })
-}
-
-func TestTaskfile_TaskfileBuilderFindArtifactsForTargetGood(t *testing.T) {
- fs := storage.Local
- builder := NewTaskfileBuilder()
-
- t.Run("finds artifacts in platform subdirectory", func(t *testing.T) {
- dir := t.TempDir()
- platformDir := ax.Join(dir, "linux_amd64")
- result := ax.MkdirAll(platformDir, 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(platformDir, "myapp"), []byte("binary"), 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- target := build.Target{OS: "linux", Arch: "amd64"}
- artifacts := builder.findArtifactsForTarget(fs, dir, target)
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual("linux", artifacts[0].OS) {
- t.Fatalf("want %v, got %v", "linux", artifacts[0].OS)
- }
- if !stdlibAssertEqual("amd64", artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", "amd64", artifacts[0].Arch)
- }
-
- })
-
- t.Run("finds artifacts by name pattern in root", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "myapp-linux-amd64"), []byte("binary"), 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- target := build.Target{OS: "linux", Arch: "amd64"}
- artifacts := builder.findArtifactsForTarget(fs, dir, target)
- if stdlibAssertEmpty(artifacts) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("returns empty when no matching artifacts", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- target := build.Target{OS: "linux", Arch: "arm64"}
- artifacts := builder.findArtifactsForTarget(fs, dir, target)
- if !stdlibAssertEmpty(artifacts) {
- t.Fatalf("expected empty, got %v", artifacts)
- }
-
- })
-
- t.Run("handles .app bundles on darwin", func(t *testing.T) {
- dir := t.TempDir()
- platformDir := ax.Join(dir, "darwin_arm64")
- appDir := ax.Join(platformDir, "MyApp.app")
- result := ax.MkdirAll(appDir, 0755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- target := build.Target{OS: "darwin", Arch: "arm64"}
- artifacts := builder.findArtifactsForTarget(fs, dir, target)
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertContains(artifacts[0].Path, "MyApp.app") {
- t.Fatalf("expected %v to contain %v", artifacts[0].Path, "MyApp.app")
- }
-
- })
-}
-
-func TestTaskfile_TaskfileBuilderMatchPatternGood(t *testing.T) {
- builder := NewTaskfileBuilder()
-
- t.Run("matches simple glob", func(t *testing.T) {
- if !(builder.matchPattern("myapp-linux-amd64", "*-linux-amd64")) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("does not match different pattern", func(t *testing.T) {
- if builder.matchPattern("myapp-linux-amd64", "*-darwin-arm64") {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("matches wildcard", func(t *testing.T) {
- if !(builder.matchPattern("test_linux_arm64.bin", "*_linux_arm64*")) {
- t.Fatal("expected true")
- }
-
- })
-}
-
-func TestTaskfile_TaskfileBuilderInterfaceGood(t *testing.T) {
- builder := NewTaskfileBuilder()
- var _ build.Builder = builder
- if !stdlibAssertEqual("taskfile", builder.Name()) {
- t.Fatalf("want %v, got %v", "taskfile", builder.Name())
- }
- detected := requireCPPBool(t, builder.Detect(nil, t.TempDir()))
- if detected {
- t.Fatal("expected empty temp directory not to be detected")
- }
-}
-
-func TestTaskfile_TaskfileBuilderResolveTaskCliGood(t *testing.T) {
- builder := NewTaskfileBuilder()
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "task")
- result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- command := requireCPPString(t, builder.resolveTaskCli(fallbackPath))
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestTaskfile_TaskfileBuilderResolveTaskCliBad(t *testing.T) {
- builder := NewTaskfileBuilder()
- t.Setenv("PATH", "")
-
- result := builder.resolveTaskCli(ax.Join(t.TempDir(), "missing-task"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "task CLI not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "task CLI not found")
- }
-
-}
-
-func TestTaskfile_TaskfileBuilderRunTaskGood(t *testing.T) {
- binDir := t.TempDir()
- taskPath := ax.Join(binDir, "task")
- logPath := ax.Join(t.TempDir(), "task.env")
-
- script := `#!/bin/sh
-set -eu
-
-env | sort > "${TASK_BUILD_LOG_FILE}"
-`
- result := ax.WriteFile(taskPath, []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("TASK_BUILD_LOG_FILE", logPath)
-
- builder := NewTaskfileBuilder()
- goCacheDir := ax.Join(t.TempDir(), "cache", "go-build")
- goModCacheDir := ax.Join(t.TempDir(), "cache", "go-mod")
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: t.TempDir(),
- OutputDir: "/tmp/out",
- Name: "sample",
- Version: "v1.2.3",
- Env: []string{"FOO=bar"},
- Cache: build.CacheConfig{
- Enabled: true,
- Paths: []string{
- goCacheDir,
- goModCacheDir,
- },
- },
- }
- result = builder.runTask(context.Background(), cfg, taskPath, cfg.OutputDir, build.Target{OS: "linux", Arch: "amd64"})
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "FOO=bar") {
- t.Fatalf("expected %v to contain %v", string(content), "FOO=bar")
- }
- if !stdlibAssertContains(string(content), "GOOS=linux") {
- t.Fatalf("expected %v to contain %v", string(content), "GOOS=linux")
- }
- if !stdlibAssertContains(string(content), "GOARCH=amd64") {
- t.Fatalf("expected %v to contain %v", string(content), "GOARCH=amd64")
- }
- if !stdlibAssertContains(string(content), "TARGET_OS=linux") {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_OS=linux")
- }
- if !stdlibAssertContains(string(content), "TARGET_ARCH=amd64") {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_ARCH=amd64")
- }
- if !stdlibAssertContains(string(content), "OUTPUT_DIR=/tmp/out") {
- t.Fatalf("expected %v to contain %v", string(content), "OUTPUT_DIR=/tmp/out")
- }
- if !stdlibAssertContains(string(content), "TARGET_DIR=/tmp/out/linux_amd64") {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_DIR=/tmp/out/linux_amd64")
- }
- if !stdlibAssertContains(string(content), "NAME=sample") {
- t.Fatalf("expected %v to contain %v", string(content), "NAME=sample")
- }
- if !stdlibAssertContains(string(content), "VERSION=v1.2.3") {
- t.Fatalf("expected %v to contain %v", string(content), "VERSION=v1.2.3")
- }
- if !stdlibAssertContains(string(content), "CGO_ENABLED=0") {
- t.Fatalf("expected %v to contain %v", string(content), "CGO_ENABLED=0")
- }
- if !stdlibAssertContains(string(content), "GOCACHE="+goCacheDir) {
- t.Fatalf("expected %v to contain %v", string(content), "GOCACHE="+goCacheDir)
- }
- if !stdlibAssertContains(string(content), "GOMODCACHE="+goModCacheDir) {
- t.Fatalf("expected %v to contain %v", string(content), "GOMODCACHE="+goModCacheDir)
- }
-
-}
-
-func TestTaskfile_TaskfileBuilderBuild_DoesNotMutateOutputDirGood(t *testing.T) {
- projectDir := t.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, "Taskfile.yml"), []byte("version: '3'\n"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- binDir := t.TempDir()
- taskPath := ax.Join(binDir, "task")
- script := `#!/bin/sh
-set -eu
-
-mkdir -p "${OUTPUT_DIR}/${GOOS}_${GOARCH}"
-printf '%s\n' "${NAME:-taskfile}" > "${OUTPUT_DIR}/${GOOS}_${GOARCH}/${NAME:-taskfile}"
-`
- result = ax.WriteFile(taskPath, []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- builder := NewTaskfileBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- Name: "sample",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEmpty(cfg.OutputDir) {
- t.Fatalf("expected empty, got %v", cfg.OutputDir)
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist"), ax.Dir(ax.Dir(artifacts[0].Path))) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist"), ax.Dir(ax.Dir(artifacts[0].Path)))
- }
-
-}
-
-func TestTaskfile_TaskfileBuilderBuildGood(t *testing.T) {
- projectDir := t.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, "Taskfile.yml"), []byte("version: '3'\n"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- binDir := t.TempDir()
- taskPath := ax.Join(binDir, "task")
- logPath := ax.Join(t.TempDir(), "task.build.env")
-
- script := `#!/bin/sh
-set -eu
-
-mkdir -p "${OUTPUT_DIR}/${GOOS}_${GOARCH}"
-printf '%s\n' "${NAME:-taskfile}" > "${OUTPUT_DIR}/${GOOS}_${GOARCH}/${NAME:-taskfile}"
-env | sort > "${TASK_BUILD_LOG_FILE}"
-`
- result = ax.WriteFile(taskPath, []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("TASK_BUILD_LOG_FILE", logPath)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- builder := NewTaskfileBuilder()
- goCacheDir := ax.Join(t.TempDir(), "cache", "go-build")
- goModCacheDir := ax.Join(t.TempDir(), "cache", "go-mod")
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- Name: "sample",
- Version: "v1.2.3",
- Env: []string{"FOO=bar"},
- Cache: build.CacheConfig{
- Enabled: true,
- Paths: []string{
- goCacheDir,
- goModCacheDir,
- },
- },
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", "linux_amd64", "sample"), artifacts[0].Path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", "linux_amd64", "sample"), artifacts[0].Path)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "FOO=bar") {
- t.Fatalf("expected %v to contain %v", string(content), "FOO=bar")
- }
- if !stdlibAssertContains(string(content), "OUTPUT_DIR="+ax.Join(projectDir, "dist")) {
- t.Fatalf("expected %v to contain %v", string(content), "OUTPUT_DIR="+ax.Join(projectDir, "dist"))
- }
- if !stdlibAssertContains(string(content), "GOOS=linux") {
- t.Fatalf("expected %v to contain %v", string(content), "GOOS=linux")
- }
- if !stdlibAssertContains(string(content), "GOARCH=amd64") {
- t.Fatalf("expected %v to contain %v", string(content), "GOARCH=amd64")
- }
- if !stdlibAssertContains(string(content), "TARGET_OS=linux") {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_OS=linux")
- }
- if !stdlibAssertContains(string(content), "TARGET_ARCH=amd64") {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_ARCH=amd64")
- }
- if !stdlibAssertContains(string(content), "TARGET_DIR="+ax.Join(projectDir, "dist", "linux_amd64")) {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_DIR="+ax.Join(projectDir, "dist", "linux_amd64"))
- }
- if !stdlibAssertContains(string(content), "CGO_ENABLED=0") {
- t.Fatalf("expected %v to contain %v", string(content), "CGO_ENABLED=0")
- }
- if !stdlibAssertContains(string(content), "GOCACHE="+goCacheDir) {
- t.Fatalf("expected %v to contain %v", string(content), "GOCACHE="+goCacheDir)
- }
- if !stdlibAssertContains(string(content), "GOMODCACHE="+goModCacheDir) {
- t.Fatalf("expected %v to contain %v", string(content), "GOMODCACHE="+goModCacheDir)
- }
-
-}
-
-func TestTaskfile_TaskfileBuilderBuild_DefaultTargetGood(t *testing.T) {
- projectDir := t.TempDir()
- result := ax.WriteFile(ax.Join(projectDir, "Taskfile.yml"), []byte("version: '3'\n"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- binDir := t.TempDir()
- taskPath := ax.Join(binDir, "task")
- logPath := ax.Join(t.TempDir(), "task.default.env")
-
- script := `#!/bin/sh
-set -eu
-
-mkdir -p "${OUTPUT_DIR}/${GOOS}_${GOARCH}"
-printf '%s\n' "${GOOS}/${GOARCH}" > "${OUTPUT_DIR}/${GOOS}_${GOARCH}/artifact"
-env | sort > "${TASK_BUILD_LOG_FILE}"
-`
- result = ax.WriteFile(taskPath, []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("TASK_BUILD_LOG_FILE", logPath)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- builder := NewTaskfileBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- Name: "sample",
- Version: "v1.2.3",
- Env: []string{"FOO=bar"},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, nil))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(ax.Join(projectDir, "dist", runtime.GOOS+"_"+runtime.GOARCH, "artifact"), artifacts[0].Path) {
- t.Fatalf("want %v, got %v", ax.Join(projectDir, "dist", runtime.GOOS+"_"+runtime.GOARCH, "artifact"), artifacts[0].Path)
- }
- if !stdlibAssertEqual(runtime.GOOS, artifacts[0].OS) {
- t.Fatalf("want %v, got %v", runtime.GOOS, artifacts[0].OS)
- }
- if !stdlibAssertEqual(runtime.GOARCH, artifacts[0].Arch) {
- t.Fatalf("want %v, got %v", runtime.GOARCH, artifacts[0].Arch)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "FOO=bar") {
- t.Fatalf("expected %v to contain %v", string(content), "FOO=bar")
- }
- if !stdlibAssertContains(string(content), "OUTPUT_DIR="+ax.Join(projectDir, "dist")) {
- t.Fatalf("expected %v to contain %v", string(content), "OUTPUT_DIR="+ax.Join(projectDir, "dist"))
- }
- if !stdlibAssertContains(string(content), "GOOS="+runtime.GOOS) {
- t.Fatalf("expected %v to contain %v", string(content), "GOOS="+runtime.GOOS)
- }
- if !stdlibAssertContains(string(content), "GOARCH="+runtime.GOARCH) {
- t.Fatalf("expected %v to contain %v", string(content), "GOARCH="+runtime.GOARCH)
- }
- if !stdlibAssertContains(string(content), "TARGET_OS="+runtime.GOOS) {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_OS="+runtime.GOOS)
- }
- if !stdlibAssertContains(string(content), "TARGET_ARCH="+runtime.GOARCH) {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_ARCH="+runtime.GOARCH)
- }
- if !stdlibAssertContains(string(content), "TARGET_DIR="+ax.Join(projectDir, "dist", runtime.GOOS+"_"+runtime.GOARCH)) {
- t.Fatalf("expected %v to contain %v", string(content), "TARGET_DIR="+ax.Join(projectDir, "dist", runtime.GOOS+"_"+runtime.GOARCH))
- }
- if !stdlibAssertContains(string(content), "CGO_ENABLED=0") {
- t.Fatalf("expected %v to contain %v", string(content), "CGO_ENABLED=0")
- }
-
-}
-
-func TestTaskfile_TaskfileBuilderRunTask_CGOEnabledGood(t *testing.T) {
- binDir := t.TempDir()
- taskPath := ax.Join(binDir, "task")
- logPath := ax.Join(t.TempDir(), "task.cgo.env")
-
- script := `#!/bin/sh
-set -eu
-
-env | sort > "${TASK_BUILD_LOG_FILE}"
-`
- result := ax.WriteFile(taskPath, []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("TASK_BUILD_LOG_FILE", logPath)
-
- builder := NewTaskfileBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: t.TempDir(),
- OutputDir: "/tmp/out",
- Name: "sample",
- Version: "v1.2.3",
- CGO: true,
- }
- result = builder.runTask(context.Background(), cfg, taskPath, cfg.OutputDir, build.Target{OS: "linux", Arch: "amd64"})
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "CGO_ENABLED=1") {
- t.Fatalf("expected %v to contain %v", string(content), "CGO_ENABLED=1")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestTaskfile_NewTaskfileBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewTaskfileBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestTaskfile_NewTaskfileBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewTaskfileBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestTaskfile_NewTaskfileBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewTaskfileBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestTaskfile_TaskfileBuilder_Name_Good(t *core.T) {
- subject := &TaskfileBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestTaskfile_TaskfileBuilder_Name_Bad(t *core.T) {
- subject := &TaskfileBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestTaskfile_TaskfileBuilder_Name_Ugly(t *core.T) {
- subject := &TaskfileBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestTaskfile_TaskfileBuilder_Detect_Good(t *core.T) {
- subject := &TaskfileBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestTaskfile_TaskfileBuilder_Detect_Bad(t *core.T) {
- subject := &TaskfileBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestTaskfile_TaskfileBuilder_Detect_Ugly(t *core.T) {
- subject := &TaskfileBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestTaskfile_TaskfileBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &TaskfileBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestTaskfile_TaskfileBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &TaskfileBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestTaskfile_TaskfileBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &TaskfileBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/wails.go b/pkg/build/builders/wails.go
deleted file mode 100644
index 1442a04..0000000
--- a/pkg/build/builders/wails.go
+++ /dev/null
@@ -1,1075 +0,0 @@
-// Package builders provides build implementations for different project types.
-package builders
-
-import (
- "context"
- stdfs "io/fs"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// WailsBuilder implements the Builder interface for Wails v3 projects.
-//
-// b := builders.NewWailsBuilder()
-type WailsBuilder struct{}
-
-// NewWailsBuilder creates a new WailsBuilder instance.
-//
-// b := builders.NewWailsBuilder()
-func NewWailsBuilder() *WailsBuilder {
- return &WailsBuilder{}
-}
-
-// Name returns the builder's identifier.
-//
-// name := b.Name() // → "wails"
-func (b *WailsBuilder) Name() string {
- return "wails"
-}
-
-// Detect checks if this builder can handle the project (checks for wails.json).
-//
-// ok, err := b.Detect(storage.Local, ".")
-func (b *WailsBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(build.IsWailsProject(fs, dir))
-}
-
-// Build compiles the Wails project for the specified targets.
-// Wails v3: delegates to Taskfile; Wails v2: uses 'wails build'.
-//
-// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "darwin", Arch: "arm64"}})
-func (b *WailsBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("WailsBuilder.Build", "config is nil", nil))
- }
- filesystem := ensureBuildFilesystem(cfg)
-
- if len(targets) == 0 {
- return core.Fail(core.E("WailsBuilder.Build", "no targets specified", nil))
- }
-
- if versionFlag := build.VersionLinkerFlag(cfg.Version); !versionFlag.OK {
- return versionFlag
- }
-
- if cfg.OutputDir == "" {
- cfg.OutputDir = ax.Join(cfg.ProjectDir, "dist")
- }
-
- // Detect Wails version
- isV3 := b.isWailsV3(filesystem, cfg.ProjectDir)
-
- if isV3 {
- // Wails v3 projects already ship Taskfiles. Prefer them when present because
- // they capture project-specific packaging logic. Fall back to the CLI when a
- // project is Wails-backed but does not expose Task targets.
- taskBuilder := NewTaskfileBuilder()
- if detected := taskBuilder.Detect(filesystem, cfg.ProjectDir); detected.OK && detected.Value.(bool) {
- return taskBuilder.Build(ctx, b.buildV3Config(cfg), targets)
- }
-
- prebuilt := b.PreBuild(ctx, cfg)
- if !prebuilt.OK {
- return prebuilt
- }
-
- var artifacts []build.Artifact
- for _, target := range targets {
- artifactResult := b.buildV3Target(ctx, cfg, target)
- if !artifactResult.OK {
- return core.Fail(core.E("WailsBuilder.Build", "failed to build "+target.String(), core.NewError(artifactResult.Error())))
- }
- artifacts = append(artifacts, artifactResult.Value.(build.Artifact))
- }
-
- return core.Ok(artifacts)
- }
-
- // Wails v2 strategy: Use 'wails build'
- prebuilt := b.PreBuild(ctx, cfg)
- if !prebuilt.OK {
- return prebuilt
- }
-
- // Ensure output directory exists
- created := filesystem.EnsureDir(cfg.OutputDir)
- if !created.OK {
- return core.Fail(core.E("WailsBuilder.Build", "failed to create output directory", core.NewError(created.Error())))
- }
-
- // Note: Wails v2 handles frontend installation/building automatically via wails.json config
-
- var artifacts []build.Artifact
-
- for _, target := range targets {
- artifactResult := b.buildV2Target(ctx, cfg, target)
- if !artifactResult.OK {
- return core.Fail(core.E("WailsBuilder.Build", "failed to build "+target.String(), core.NewError(artifactResult.Error())))
- }
- artifacts = append(artifacts, artifactResult.Value.(build.Artifact))
- }
-
- return core.Ok(artifacts)
-}
-
-// buildV3Config returns a copy of the build config with Wails v3 requirements applied.
-func (b *WailsBuilder) buildV3Config(cfg *build.Config) *build.Config {
- if cfg == nil {
- return nil
- }
-
- v3Config := *cfg
- v3Config.CGO = true
- return &v3Config
-}
-
-// buildV3Target builds a Wails v3 project for a single target using the wails3 CLI.
-func (b *WailsBuilder) buildV3Target(ctx context.Context, cfg *build.Config, target build.Target) core.Result {
- filesystem := ensureBuildFilesystem(cfg)
-
- wailsCommandResult := b.resolveWails3Cli()
- if !wailsCommandResult.OK {
- return wailsCommandResult
- }
- wailsCommand := wailsCommandResult.Value.(string)
-
- binaryName := cfg.Name
- if binaryName == "" {
- binaryName = ax.Base(cfg.ProjectDir)
- }
-
- verb := "build"
- args := []string{verb, "GOOS=" + target.OS, "GOARCH=" + target.Arch}
- if cfg.NSIS && target.OS == "windows" {
- verb = "package"
- args[0] = verb
- }
- taskVarsResult := buildV3TaskVars(cfg, target)
- if !taskVarsResult.OK {
- return taskVarsResult
- }
- taskVars := taskVarsResult.Value.([]string)
- args = append(args, taskVars...)
-
- env := appendConfiguredEnv(cfg,
- core.Sprintf("GOOS=%s", target.OS),
- core.Sprintf("GOARCH=%s", target.Arch),
- core.Sprintf("TARGET_OS=%s", target.OS),
- core.Sprintf("TARGET_ARCH=%s", target.Arch),
- core.Sprintf("OUTPUT_DIR=%s", cfg.OutputDir),
- )
- if cfg.Version != "" {
- env = append(env, core.Sprintf("VERSION=%s", cfg.Version))
- }
- if binaryName != "" {
- env = append(env, core.Sprintf("NAME=%s", binaryName))
- }
- goflagsResult := buildV3GoFlags(cfg)
- if !goflagsResult.OK {
- return goflagsResult
- }
- if goflags := goflagsResult.Value.(string); goflags != "" {
- env = append(env, "GOFLAGS="+goflags)
- }
- if cfg.CGO {
- env = append(env, "CGO_ENABLED=1")
- }
- cleanup := func() {}
- if cfg.Obfuscate {
- obfuscationResult := b.prepareV3Obfuscation(env)
- if !obfuscationResult.OK {
- return obfuscationResult
- }
- obfuscation := obfuscationResult.Value.(obfuscationEnv)
- env = obfuscation.env
- cleanup = obfuscation.cleanup
- defer cleanup()
- }
-
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, env, wailsCommand, args...)
- if !output.OK {
- return core.Fail(core.E("WailsBuilder.buildV3Target", "wails3 "+verb+" failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- sourcePathResult := b.findV3Artifact(filesystem, cfg.ProjectDir, binaryName, target, verb == "package")
- if !sourcePathResult.OK {
- return sourcePathResult
- }
- sourcePath := sourcePathResult.Value.(string)
-
- platformDir := ax.Join(cfg.OutputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
- created := filesystem.EnsureDir(platformDir)
- if !created.OK {
- return core.Fail(core.E("WailsBuilder.buildV3Target", "failed to create output dir", core.NewError(created.Error())))
- }
-
- destPath := ax.Join(platformDir, ax.Base(sourcePath))
- copied := copyBuildArtifact(filesystem, sourcePath, destPath)
- if !copied.OK {
- return core.Fail(core.E("WailsBuilder.buildV3Target", "failed to copy artifact "+sourcePath, core.NewError(copied.Error())))
- }
-
- return core.Ok(build.Artifact{
- Path: destPath,
- OS: target.OS,
- Arch: target.Arch,
- })
-}
-
-// PreBuild runs the frontend build step before Wails compiles the desktop app.
-//
-// err := b.PreBuild(ctx, cfg) // runs `deno task build` or `npm run build`
-func (b *WailsBuilder) PreBuild(ctx context.Context, cfg *build.Config) core.Result {
- if cfg == nil {
- return core.Fail(core.E("WailsBuilder.PreBuild", "config is nil", nil))
- }
-
- frontendResult := b.resolveFrontendBuild(cfg)
- if !frontendResult.OK {
- return frontendResult
- }
- frontend := frontendResult.Value.(frontendBuild)
- frontendDir := frontend.dir
- command := frontend.command
- args := frontend.args
- if command == "" {
- return core.Ok(nil)
- }
-
- output := ax.CombinedOutput(ctx, frontendDir, build.BuildEnvironment(cfg), command, args...)
- if !output.OK {
- return core.Fail(core.E("WailsBuilder.PreBuild", command+" build failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// isWailsV3 checks if the project uses Wails v3 by inspecting go.mod.
-func (b *WailsBuilder) isWailsV3(fs storage.Medium, dir string) bool {
- goModPath := ax.Join(dir, "go.mod")
- content := fs.Read(goModPath)
- if !content.OK {
- return false
- }
- return core.Contains(content.Value.(string), "github.com/wailsapp/wails/v3")
-}
-
-// resolveFrontendBuild selects the frontend directory and build command.
-//
-// dir, command, args, err := b.resolveFrontendBuild(cfg)
-type frontendBuild struct {
- dir string
- command string
- args []string
-}
-
-func (b *WailsBuilder) resolveFrontendBuild(cfg *build.Config) core.Result {
- if cfg == nil {
- return core.Fail(core.E("WailsBuilder.resolveFrontendBuild", "config is nil", nil))
- }
-
- fs := cfg.FS
- if fs == nil {
- fs = storage.Local
- }
- projectDir := cfg.ProjectDir
- frontendDir := b.resolveFrontendDir(fs, projectDir)
- if frontendDir == "" {
- if build.DenoRequested(cfg.DenoBuild) {
- if fs.IsDir(ax.Join(projectDir, "frontend")) {
- frontendDir = ax.Join(projectDir, "frontend")
- } else {
- frontendDir = projectDir
- }
- } else {
- return core.Ok(frontendBuild{})
- }
- }
-
- if b.hasDenoConfig(fs, frontendDir) || build.DenoRequested(cfg.DenoBuild) {
- resolved := resolveDenoBuildCommand(cfg, b.resolveDenoCli)
- if !resolved.OK {
- return resolved
- }
- spec := resolved.Value.(commandSpec)
- return core.Ok(frontendBuild{dir: frontendDir, command: spec.command, args: spec.args})
- }
-
- if build.NpmRequested(cfg.NpmBuild) {
- resolved := resolveNpmBuildCommand(cfg, b.resolveNpmCli)
- if !resolved.OK {
- return resolved
- }
- spec := resolved.Value.(commandSpec)
- return core.Ok(frontendBuild{dir: frontendDir, command: spec.command, args: spec.args})
- }
-
- if fs.IsFile(ax.Join(frontendDir, "package.json")) {
- packageManager := detectPackageManager(fs, frontendDir)
- return b.resolvePackageManagerBuild(frontendDir, packageManager)
- }
-
- return core.Ok(frontendBuild{})
-}
-
-// resolvePackageManagerBuild returns the frontend build command for a detected package manager.
-func (b *WailsBuilder) resolvePackageManagerBuild(frontendDir, packageManager string) core.Result {
- switch packageManager {
- case "bun":
- command := b.resolveBunCli()
- if !command.OK {
- return command
- }
- return core.Ok(frontendBuild{dir: frontendDir, command: command.Value.(string), args: []string{"run", "build"}})
- case "pnpm":
- command := b.resolvePnpmCli()
- if !command.OK {
- return command
- }
- return core.Ok(frontendBuild{dir: frontendDir, command: command.Value.(string), args: []string{"run", "build"}})
- case "yarn":
- command := b.resolveYarnCli()
- if !command.OK {
- return command
- }
- return core.Ok(frontendBuild{dir: frontendDir, command: command.Value.(string), args: []string{"build"}})
- default:
- command := b.resolveNpmCli()
- if !command.OK {
- return command
- }
- return core.Ok(frontendBuild{dir: frontendDir, command: command.Value.(string), args: []string{"run", "build"}})
- }
-}
-
-// resolveFrontendDir returns the directory that contains the frontend build manifest.
-func (b *WailsBuilder) resolveFrontendDir(fs storage.Medium, projectDir string) string {
- frontendDir := ax.Join(projectDir, "frontend")
- if fs.IsDir(frontendDir) && (b.hasDenoConfig(fs, frontendDir) || fs.IsFile(ax.Join(frontendDir, "package.json"))) {
- return frontendDir
- }
-
- if b.hasDenoConfig(fs, projectDir) || fs.IsFile(ax.Join(projectDir, "package.json")) {
- return projectDir
- }
-
- if nestedFrontendDir := b.resolveSubtreeFrontendDir(fs, projectDir); nestedFrontendDir != "" {
- return nestedFrontendDir
- }
-
- if build.DenoRequested("") {
- if fs.IsDir(frontendDir) {
- return frontendDir
- }
- return projectDir
- }
-
- return ""
-}
-
-// hasDenoConfig reports whether the frontend directory contains a Deno manifest.
-func (b *WailsBuilder) hasDenoConfig(fs storage.Medium, dir string) bool {
- return fs.IsFile(ax.Join(dir, "deno.json")) || fs.IsFile(ax.Join(dir, "deno.jsonc"))
-}
-
-// resolveSubtreeFrontendDir finds a nested frontend manifest within the project tree.
-// This supports monorepo layouts such as apps/web/package.json or apps/web/deno.json
-// when frontend/ is absent.
-func (b *WailsBuilder) resolveSubtreeFrontendDir(fs storage.Medium, projectDir string) string {
- return b.findFrontendDir(fs, projectDir, 0)
-}
-
-// findFrontendDir walks nested directories until it finds a frontend manifest.
-// The v3 discovery contract only scans to depth 2 for monorepo frontends.
-func (b *WailsBuilder) findFrontendDir(fs storage.Medium, dir string, depth int) string {
- if depth >= 2 {
- return ""
- }
-
- entriesResult := fs.List(dir)
- if !entriesResult.OK {
- return ""
- }
- entries := entriesResult.Value.([]stdfs.DirEntry)
-
- for _, entry := range entries {
- if !entry.IsDir() {
- continue
- }
-
- name := entry.Name()
- if name == "node_modules" || core.HasPrefix(name, ".") {
- continue
- }
-
- candidateDir := ax.Join(dir, name)
- if b.hasDenoConfig(fs, candidateDir) || fs.IsFile(ax.Join(candidateDir, "package.json")) {
- return candidateDir
- }
-
- if nested := b.findFrontendDir(fs, candidateDir, depth+1); nested != "" {
- return nested
- }
- }
-
- return ""
-}
-
-// buildV2Target compiles for a single target platform using wails (v2).
-func (b *WailsBuilder) buildV2Target(ctx context.Context, cfg *build.Config, target build.Target) core.Result {
- filesystem := ensureBuildFilesystem(cfg)
-
- if cfg.WebView2 != "" && target.OS == "windows" {
- valid := validateWebView2Mode(cfg.WebView2)
- if !valid.OK {
- return valid
- }
- }
-
- wailsCommandResult := b.resolveWailsCli()
- if !wailsCommandResult.OK {
- return wailsCommandResult
- }
- wailsCommand := wailsCommandResult.Value.(string)
-
- // Determine output binary name
- binaryName := cfg.Name
- if binaryName == "" {
- binaryName = ax.Base(cfg.ProjectDir)
- }
-
- // Build the wails build arguments
- args := []string{"build"}
-
- // Honour the action/CLI build-name override by forwarding it to Wails v2.
- if binaryName != "" {
- args = append(args, "-o", binaryName)
- }
-
- if len(cfg.BuildTags) > 0 {
- args = append(args, "-tags", core.Join(",", cfg.BuildTags...))
- }
-
- ldflags := append([]string{}, cfg.LDFlags...)
- if cfg.Version != "" && !hasVersionLDFlag(ldflags) {
- versionFlag := build.VersionLinkerFlag(cfg.Version)
- if !versionFlag.OK {
- return versionFlag
- }
- ldflags = append(ldflags, versionFlag.Value.(string))
- }
- if len(ldflags) > 0 {
- args = append(args, "-ldflags", core.Join(" ", ldflags...))
- }
-
- if cfg.Obfuscate {
- args = append(args, "-obfuscated")
- }
-
- if cfg.NSIS && target.OS == "windows" {
- args = append(args, "-nsis")
- }
-
- if cfg.WebView2 != "" && target.OS == "windows" {
- args = append(args, "-webview2", cfg.WebView2)
- }
-
- // Platform
- args = append(args, "-platform", core.Sprintf("%s/%s", target.OS, target.Arch))
-
- // Output (Wails v2 uses -o for the binary name, relative to build/bin usually, but we want to control it)
- // Actually, Wails v2 is opinionated about output dir (build/bin).
- // We might need to copy artifacts after build if we want them in cfg.OutputDir.
- // For now, let's try to let Wails do its thing and find the artifact.
-
- // Capture output for error messages
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, build.BuildEnvironment(cfg), wailsCommand, args...)
- if !output.OK {
- return core.Fail(core.E("WailsBuilder.buildV2Target", "wails build failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- // Wails v2 typically outputs to build/bin
- // We need to move/copy it to our desired output dir
-
- // Construct the source path where Wails v2 puts the binary
- wailsOutputDir := ax.Join(cfg.ProjectDir, "build", "bin")
-
- // Find the artifact in Wails output dir
- sourcePathResult := b.findArtifact(filesystem, wailsOutputDir, binaryName, target)
- if !sourcePathResult.OK {
- return core.Fail(core.E("WailsBuilder.buildV2Target", "failed to find Wails v2 build artifact", core.NewError(sourcePathResult.Error())))
- }
- sourcePath := sourcePathResult.Value.(string)
-
- // Move/Copy to our output dir
- // Create platform specific dir in our output
- platformDir := ax.Join(cfg.OutputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
- created := filesystem.EnsureDir(platformDir)
- if !created.OK {
- return core.Fail(core.E("WailsBuilder.buildV2Target", "failed to create output dir", core.NewError(created.Error())))
- }
-
- destPath := ax.Join(platformDir, ax.Base(sourcePath))
-
- // Copy the selected artifact, preserving directory bundles such as .app packages.
- copied := copyBuildArtifact(filesystem, sourcePath, destPath)
- if !copied.OK {
- return core.Fail(core.E("WailsBuilder.buildV2Target", "failed to copy artifact "+sourcePath, core.NewError(copied.Error())))
- }
-
- return core.Ok(build.Artifact{
- Path: destPath,
- OS: target.OS,
- Arch: target.Arch,
- })
-}
-
-// findArtifact locates the built artifact based on the target platform.
-func (b *WailsBuilder) findArtifact(fs storage.Medium, platformDir, binaryName string, target build.Target) core.Result {
- var candidates []string
-
- switch target.OS {
- case "windows":
- // Look for NSIS installer first, then plain exe
- candidates = []string{
- ax.Join(platformDir, binaryName+"-installer.exe"),
- ax.Join(platformDir, binaryName+".exe"),
- ax.Join(platformDir, binaryName+"-amd64-installer.exe"),
- }
- case "darwin":
- // Look for .dmg, then .app bundle, then plain binary
- candidates = []string{
- ax.Join(platformDir, binaryName+".dmg"),
- ax.Join(platformDir, binaryName+".app"),
- ax.Join(platformDir, binaryName),
- }
- default:
- // Linux and others: look for plain binary
- candidates = []string{
- ax.Join(platformDir, binaryName),
- }
- }
-
- // Try each candidate
- for _, candidate := range candidates {
- if fs.Exists(candidate) {
- return core.Ok(candidate)
- }
- }
-
- // If no specific candidate found, try to find any executable or package in the directory
- entriesResult := fs.List(platformDir)
- if !entriesResult.OK {
- return core.Fail(core.E("WailsBuilder.findArtifact", "failed to read platform directory", core.NewError(entriesResult.Error())))
- }
- entries := entriesResult.Value.([]stdfs.DirEntry)
-
- for _, entry := range entries {
- name := entry.Name()
- // Skip common non-artifact files
- if core.HasSuffix(name, ".go") || core.HasSuffix(name, ".json") {
- continue
- }
-
- path := ax.Join(platformDir, name)
- info, err := entry.Info()
- if err != nil {
- continue
- }
-
- // On Unix, check if it's executable; on Windows, check for .exe
- if target.OS == "windows" {
- if core.HasSuffix(name, ".exe") {
- return core.Ok(path)
- }
- } else if info.Mode()&0111 != 0 || entry.IsDir() {
- // Executable file or directory (.app bundle)
- return core.Ok(path)
- }
- }
-
- return core.Fail(core.E("WailsBuilder.findArtifact", "no artifact found in "+platformDir, nil))
-}
-
-func (b *WailsBuilder) findV3Artifact(fs storage.Medium, projectDir, binaryName string, target build.Target, packaged bool) core.Result {
- if packaged && target.OS == "windows" {
- for _, candidate := range []string{
- ax.Join(projectDir, "build", "windows", "nsis", binaryName+"-installer.exe"),
- ax.Join(projectDir, "bin", binaryName+"-installer.exe"),
- } {
- if fs.Exists(candidate) {
- return core.Ok(candidate)
- }
- }
- }
-
- for _, platformDir := range []string{
- ax.Join(projectDir, "build", "bin"),
- ax.Join(projectDir, "bin"),
- } {
- path := b.findArtifact(fs, platformDir, binaryName, target)
- if path.OK {
- return path
- }
- }
-
- return core.Fail(core.E("WailsBuilder.findV3Artifact", "no artifact found for "+target.String(), nil))
-}
-
-// copyBuildArtifact copies a file or directory artifact into the build output tree.
-//
-// err := copyBuildArtifact(storage.Local, "/tmp/source.app", "/tmp/dist/source.app")
-func copyBuildArtifact(fs storage.Medium, sourcePath, destPath string) core.Result {
- if fs.IsDir(sourcePath) {
- created := fs.EnsureDir(destPath)
- if !created.OK {
- return created
- }
-
- entriesResult := fs.List(sourcePath)
- if !entriesResult.OK {
- return entriesResult
- }
- entries := entriesResult.Value.([]stdfs.DirEntry)
-
- for _, entry := range entries {
- childSource := ax.Join(sourcePath, entry.Name())
- childDest := ax.Join(destPath, entry.Name())
- copied := copyBuildArtifact(fs, childSource, childDest)
- if !copied.OK {
- return copied
- }
- }
-
- return core.Ok(nil)
- }
-
- infoResult := fs.Stat(sourcePath)
- if !infoResult.OK {
- return infoResult
- }
- info := infoResult.Value.(stdfs.FileInfo)
-
- content := fs.Read(sourcePath)
- if !content.OK {
- return content
- }
-
- written := fs.WriteMode(destPath, content.Value.(string), info.Mode().Perm())
- if !written.OK {
- return written
- }
-
- return core.Ok(nil)
-}
-
-// resolveWailsCli returns the executable path for the wails CLI.
-func (b *WailsBuilder) resolveWailsCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/wails",
- "/opt/homebrew/bin/wails",
- }
-
- if home := core.Env("HOME"); home != "" {
- paths = append(paths, ax.Join(home, "go", "bin", "wails"))
- }
- }
-
- command := ax.ResolveCommand("wails", paths...)
- if !command.OK {
- return core.Fail(core.E("WailsBuilder.resolveWailsCli", "wails CLI not found. Install it with: go install github.com/wailsapp/wails/v2/cmd/wails@latest", core.NewError(command.Error())))
- }
-
- return command
-}
-
-func (b *WailsBuilder) resolveWails3Cli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/wails3",
- "/opt/homebrew/bin/wails3",
- }
-
- if home := core.Env("HOME"); home != "" {
- paths = append(paths, ax.Join(home, "go", "bin", "wails3"))
- }
- }
-
- command := ax.ResolveCommand("wails3", paths...)
- if !command.OK {
- return core.Fail(core.E("WailsBuilder.resolveWails3Cli", "wails3 CLI not found. Install Wails v3 or expose it on PATH.", core.NewError(command.Error())))
- }
-
- return command
-}
-
-func buildV3GoFlags(cfg *build.Config) core.Result {
- if cfg == nil {
- return core.Ok("")
- }
-
- var flags []string
- if !containsString(cfg.Flags, "-trimpath") {
- flags = append(flags, "-trimpath")
- }
- flags = append(flags, cfg.Flags...)
-
- if len(cfg.BuildTags) > 0 {
- flags = append(flags, "-tags="+core.Join(",", cfg.BuildTags...))
- }
-
- ldflags := append([]string{}, cfg.LDFlags...)
- if cfg.Version != "" && !hasVersionLDFlag(ldflags) {
- versionFlag := build.VersionLinkerFlag(cfg.Version)
- if !versionFlag.OK {
- return versionFlag
- }
- ldflags = append(ldflags, versionFlag.Value.(string))
- }
- if len(ldflags) > 0 {
- flags = append(flags, "-ldflags="+core.Join(" ", ldflags...))
- }
-
- return core.Ok(core.Join(" ", flags...))
-}
-
-func buildV3TaskVars(cfg *build.Config, target build.Target) core.Result {
- if cfg == nil {
- return core.Ok([]string(nil))
- }
-
- var taskVars []string
- buildFlagsResult := buildV3BuildFlags(cfg, target)
- if !buildFlagsResult.OK {
- return buildFlagsResult
- }
- if buildFlags := buildFlagsResult.Value.(string); buildFlags != "" {
- taskVars = append(taskVars, "BUILD_FLAGS="+buildFlags)
- }
- if len(cfg.BuildTags) > 0 {
- taskVars = append(taskVars, "EXTRA_TAGS="+core.Join(",", deduplicateStrings(append([]string{}, cfg.BuildTags...))...))
- }
-
- if target.OS == "windows" && cfg.WebView2 != "" {
- valid := validateWebView2Mode(cfg.WebView2)
- if !valid.OK {
- return valid
- }
- taskVars = append(taskVars, "WEBVIEW2_MODE="+cfg.WebView2)
- }
-
- return core.Ok(taskVars)
-}
-
-func buildV3BuildFlags(cfg *build.Config, target build.Target) core.Result {
- if cfg == nil {
- return core.Ok("")
- }
-
- var flags []string
-
- tags := deduplicateStrings(append([]string{"production"}, cfg.BuildTags...))
- if len(tags) > 0 {
- flags = append(flags, "-tags", core.Join(",", tags...))
- }
-
- if !containsString(cfg.Flags, "-trimpath") {
- flags = append(flags, "-trimpath")
- }
- flags = append(flags, cfg.Flags...)
- if !hasFlagPrefix(cfg.Flags, "-buildvcs") {
- flags = append(flags, "-buildvcs=false")
- }
-
- ldflags := append([]string{}, cfg.LDFlags...)
- if target.OS == "windows" && !hasWindowsGUIFlag(ldflags) {
- ldflags = append(ldflags, "-H windowsgui")
- }
- if cfg.Version != "" && !hasVersionLDFlag(ldflags) {
- versionFlag := build.VersionLinkerFlag(cfg.Version)
- if !versionFlag.OK {
- return versionFlag
- }
- ldflags = append(ldflags, versionFlag.Value.(string))
- }
- if len(ldflags) > 0 {
- flags = append(flags, `-ldflags="`+core.Join(" ", ldflags...)+`"`)
- }
-
- return core.Ok(core.Join(" ", flags...))
-}
-
-type obfuscationEnv struct {
- env []string
- cleanup func()
-}
-
-func (b *WailsBuilder) prepareV3Obfuscation(env []string) core.Result {
- garbleCommandResult := (&GoBuilder{}).resolveGarbleCli()
- if !garbleCommandResult.OK {
- return garbleCommandResult
- }
- garbleCommand := garbleCommandResult.Value.(string)
- goCommandResult := resolveGoCli()
- if !goCommandResult.OK {
- return goCommandResult
- }
- goCommand := goCommandResult.Value.(string)
-
- shimDirResult := ax.TempDir("core-build-wails3-go-*")
- if !shimDirResult.OK {
- return core.Fail(core.E("WailsBuilder.prepareV3Obfuscation", "failed to create garble shim directory", core.NewError(shimDirResult.Error())))
- }
- shimDir := shimDirResult.Value.(string)
-
- written := writeGoShim(shimDir, goCommand, garbleCommand)
- if !written.OK {
- cleaned := ax.RemoveAll(shimDir)
- if !cleaned.OK {
- return core.Fail(core.E("WailsBuilder.prepareV3Obfuscation", "failed to clean up garble shim directory", core.NewError(cleaned.Error())))
- }
- return written
- }
-
- return core.Ok(obfuscationEnv{
- env: prependPathEnv(env, shimDir),
- cleanup: func() {
- ax.RemoveAll(shimDir)
- },
- })
-}
-
-func resolveGoCli() core.Result {
- paths := []string{
- "/usr/local/go/bin/go",
- "/opt/homebrew/bin/go",
- }
-
- if goroot := core.Env("GOROOT"); goroot != "" {
- paths = append(paths, ax.Join(goroot, "bin", "go"))
- }
-
- command := ax.ResolveCommand("go", paths...)
- if !command.OK {
- return core.Fail(core.E("WailsBuilder.resolveGoCli", "go CLI not found. Install Go from https://go.dev/dl/", core.NewError(command.Error())))
- }
-
- return command
-}
-
-func writeGoShim(dir, goCommand, garbleCommand string) core.Result {
- switch runtime.GOOS {
- case "windows":
- content := "@echo off\r\n" +
- "if \"%1\"==\"build\" (\r\n" +
- " \"" + garbleCommand + "\" %*\r\n" +
- " exit /b %errorlevel%\r\n" +
- ")\r\n" +
- "\"" + goCommand + "\" %*\r\n"
- for _, name := range []string{"go.bat", "go.cmd"} {
- written := ax.WriteFile(ax.Join(dir, name), []byte(content), 0o755)
- if !written.OK {
- return core.Fail(core.E("WailsBuilder.writeGoShim", "failed to write Windows go shim", core.NewError(written.Error())))
- }
- }
- default:
- content := "#!/bin/sh\nset -eu\nif [ \"${1:-}\" = \"build\" ]; then\n exec \"" + garbleCommand + "\" \"$@\"\nfi\nexec \"" + goCommand + "\" \"$@\"\n"
- written := ax.WriteFile(ax.Join(dir, "go"), []byte(content), 0o755)
- if !written.OK {
- return core.Fail(core.E("WailsBuilder.writeGoShim", "failed to write go shim", core.NewError(written.Error())))
- }
- }
-
- return core.Ok(nil)
-}
-
-func prependPathEnv(env []string, dir string) []string {
- pathSeparator := string(core.PathListSeparator)
- for i, entry := range env {
- if core.HasPrefix(entry, "PATH=") {
- current := core.TrimPrefix(entry, "PATH=")
- if current == "" {
- env[i] = "PATH=" + dir
- } else {
- env[i] = "PATH=" + dir + pathSeparator + current
- }
- return env
- }
- }
-
- currentPath := core.Env("PATH")
- if currentPath == "" {
- return append(env, "PATH="+dir)
- }
-
- return append(env, "PATH="+dir+pathSeparator+currentPath)
-}
-
-func hasFlagPrefix(flags []string, prefix string) bool {
- for _, flag := range flags {
- if core.HasPrefix(flag, prefix) {
- return true
- }
- }
- return false
-}
-
-func hasWindowsGUIFlag(ldflags []string) bool {
- for _, flag := range ldflags {
- if core.Contains(flag, "-H windowsgui") || core.Contains(flag, "-H=windowsgui") {
- return true
- }
- }
- return false
-}
-
-func deduplicateStrings(values []string) []string {
- if len(values) == 0 {
- return nil
- }
-
- seen := map[string]struct{}{}
- result := make([]string, 0, len(values))
- for _, value := range values {
- if value == "" {
- continue
- }
- if _, ok := seen[value]; ok {
- continue
- }
- seen[value] = struct{}{}
- result = append(result, value)
- }
- return result
-}
-
-func validateWebView2Mode(mode string) core.Result {
- switch mode {
- case "", "download", "embed", "browser", "error":
- return core.Ok(nil)
- default:
- return core.Fail(core.E("WailsBuilder.validateWebView2Mode", "webview2 must be one of download, embed, browser, or error", nil))
- }
-}
-
-// resolveDenoCli returns the executable path for the deno CLI.
-func (b *WailsBuilder) resolveDenoCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/deno",
- "/opt/homebrew/bin/deno",
- }
- }
-
- command := ax.ResolveCommand("deno", paths...)
- if !command.OK {
- return core.Fail(core.E("WailsBuilder.resolveDenoCli", "deno CLI not found. Install it from https://deno.com/runtime", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// resolveNpmCli returns the executable path for the npm CLI.
-func (b *WailsBuilder) resolveNpmCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/npm",
- "/opt/homebrew/bin/npm",
- }
- }
-
- command := ax.ResolveCommand("npm", paths...)
- if !command.OK {
- return core.Fail(core.E("WailsBuilder.resolveNpmCli", "npm CLI not found. Install Node.js from https://nodejs.org/", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// resolveBunCli returns the executable path for the bun CLI.
-func (b *WailsBuilder) resolveBunCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/bun",
- "/opt/homebrew/bin/bun",
- }
- }
-
- command := ax.ResolveCommand("bun", paths...)
- if !command.OK {
- return core.Fail(core.E("WailsBuilder.resolveBunCli", "bun CLI not found. Install it from https://bun.sh/", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// resolvePnpmCli returns the executable path for the pnpm CLI.
-func (b *WailsBuilder) resolvePnpmCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/pnpm",
- "/opt/homebrew/bin/pnpm",
- }
- }
-
- command := ax.ResolveCommand("pnpm", paths...)
- if !command.OK {
- return core.Fail(core.E("WailsBuilder.resolvePnpmCli", "pnpm CLI not found. Install it from https://pnpm.io/installation", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// resolveYarnCli returns the executable path for the yarn CLI.
-func (b *WailsBuilder) resolveYarnCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/yarn",
- "/opt/homebrew/bin/yarn",
- }
- }
-
- command := ax.ResolveCommand("yarn", paths...)
- if !command.OK {
- return core.Fail(core.E("WailsBuilder.resolveYarnCli", "yarn CLI not found. Install it from https://yarnpkg.com/getting-started/install", core.NewError(command.Error())))
- }
-
- return command
-}
-
-// detectPackageManager detects the frontend package manager based on lock files.
-// Returns "bun", "pnpm", "yarn", or "npm" (default).
-func detectPackageManager(fs storage.Medium, dir string) string {
- if declared := detectDeclaredPackageManager(fs, dir); declared != "" {
- return declared
- }
-
- // Check in priority order: bun, pnpm, yarn, npm
- lockFiles := []struct {
- file string
- manager string
- }{
- {"bun.lock", "bun"},
- {"bun.lockb", "bun"},
- {"pnpm-lock.yaml", "pnpm"},
- {"yarn.lock", "yarn"},
- {"package-lock.json", "npm"},
- }
-
- for _, lf := range lockFiles {
- if fs.IsFile(ax.Join(dir, lf.file)) {
- return lf.manager
- }
- }
-
- // Default to npm if no lock file found
- return "npm"
-}
-
-// Ensure WailsBuilder implements the Builder interface.
-var _ build.Builder = (*WailsBuilder)(nil)
diff --git a/pkg/build/builders/wails_example_test.go b/pkg/build/builders/wails_example_test.go
deleted file mode 100644
index 3507172..0000000
--- a/pkg/build/builders/wails_example_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package builders
-
-import core "dappco.re/go"
-
-// ExampleNewWailsBuilder references NewWailsBuilder on this package API surface.
-func ExampleNewWailsBuilder() {
- _ = NewWailsBuilder
- core.Println("NewWailsBuilder")
- // Output: NewWailsBuilder
-}
-
-// ExampleWailsBuilder_Name references WailsBuilder.Name on this package API surface.
-func ExampleWailsBuilder_Name() {
- _ = (*WailsBuilder).Name
- core.Println("WailsBuilder.Name")
- // Output: WailsBuilder.Name
-}
-
-// ExampleWailsBuilder_Detect references WailsBuilder.Detect on this package API surface.
-func ExampleWailsBuilder_Detect() {
- _ = (*WailsBuilder).Detect
- core.Println("WailsBuilder.Detect")
- // Output: WailsBuilder.Detect
-}
-
-// ExampleWailsBuilder_Build references WailsBuilder.Build on this package API surface.
-func ExampleWailsBuilder_Build() {
- _ = (*WailsBuilder).Build
- core.Println("WailsBuilder.Build")
- // Output: WailsBuilder.Build
-}
-
-// ExampleWailsBuilder_PreBuild references WailsBuilder.PreBuild on this package API surface.
-func ExampleWailsBuilder_PreBuild() {
- _ = (*WailsBuilder).PreBuild
- core.Println("WailsBuilder.PreBuild")
- // Output: WailsBuilder.PreBuild
-}
diff --git a/pkg/build/builders/wails_test.go b/pkg/build/builders/wails_test.go
deleted file mode 100644
index fe3f308..0000000
--- a/pkg/build/builders/wails_test.go
+++ /dev/null
@@ -1,2207 +0,0 @@
-package builders
-
-import (
- "context"
- stdfs "io/fs"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// setupWailsTestProject creates a minimal Wails project structure for testing.
-func setupWailsTestProject(t *testing.T) string {
- t.Helper()
- dir := t.TempDir()
-
- // Create wails.json
- wailsJSON := `{
- "name": "testapp",
- "outputfilename": "testapp"
-}`
- result := ax.WriteFile(ax.Join(dir, "wails.json"), []byte(wailsJSON), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- goMod := `module testapp
-
-go 1.21
-
-require github.com/wailsapp/wails/v3 v3.0.0
-`
- result = ax.WriteFile(ax.Join(dir, "go.mod"), []byte(goMod), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- mainGo := `package main
-
-func main() {
- println("hello wails")
-}
-`
- result = ax.WriteFile(ax.Join(dir, "main.go"), []byte(mainGo), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- taskfile := `version: '3'
-tasks:
- build:
- cmds:
- - mkdir -p {{.OUTPUT_DIR}}/{{.GOOS}}_{{.GOARCH}}
- - touch {{.OUTPUT_DIR}}/{{.GOOS}}_{{.GOARCH}}/testapp
-`
- result = ax.WriteFile(ax.Join(dir, "Taskfile.yml"), []byte(taskfile), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return dir
-}
-
-func setupWailsV2TestProject(t *testing.T) string {
- t.Helper()
- dir := t.TempDir()
-
- // wails.json
- result := ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- goMod := `module testapp
-go 1.21
-require github.com/wailsapp/wails/v2 v2.8.0
-`
- result = ax.WriteFile(ax.Join(dir, "go.mod"), []byte(goMod), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return dir
-}
-
-func setupFakeWailsToolchain(t *testing.T, binDir string) {
- t.Helper()
-
- wailsScript := `#!/bin/sh
-set -eu
-
-log_file="${WAILS_BUILD_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$@" > "$log_file"
- if [ -n "${GOCACHE:-}" ]; then
- printf '%s\n' "GOCACHE=${GOCACHE}" >> "$log_file"
- fi
- if [ -n "${GOMODCACHE:-}" ]; then
- printf '%s\n' "GOMODCACHE=${GOMODCACHE}" >> "$log_file"
- fi
-fi
-
-sequence_file="${BUILD_SEQUENCE_FILE:-}"
-if [ -n "$sequence_file" ]; then
- printf '%s\n' "wails" >> "$sequence_file"
- printf '%s\n' "$@" >> "$sequence_file"
- if [ -n "${CUSTOM_ENV:-}" ]; then
- printf '%s\n' "CUSTOM_ENV=${CUSTOM_ENV}" >> "$sequence_file"
- fi
-fi
-
-output_dir="build/bin"
-binary_name="testapp"
-mkdir -p "$output_dir"
-platform=""
-use_nsis=0
-
-while [ "$#" -gt 0 ]; do
- case "$1" in
- -platform)
- shift
- platform="${1:-}"
- ;;
- -o)
- shift
- binary_name="${1:-}"
- ;;
- -nsis)
- use_nsis=1
- ;;
- esac
- shift || true
-done
-
-target_os="${platform%%/*}"
-
-case "$target_os" in
- windows)
- if [ "$use_nsis" -eq 1 ]; then
- printf 'fake wails installer\n' > "$output_dir/${binary_name}-installer.exe"
- chmod +x "$output_dir/${binary_name}-installer.exe"
- else
- printf 'fake wails binary\n' > "$output_dir/${binary_name}.exe"
- chmod +x "$output_dir/${binary_name}.exe"
- fi
- ;;
- darwin)
- mkdir -p "$output_dir/${binary_name}.app/Contents/MacOS"
- printf 'fake wails binary\n' > "$output_dir/${binary_name}.app/Contents/MacOS/${binary_name}"
- chmod +x "$output_dir/${binary_name}.app/Contents/MacOS/${binary_name}"
- ;;
- *)
- printf 'fake wails binary\n' > "$output_dir/$binary_name"
- chmod +x "$output_dir/$binary_name"
- ;;
-esac
-`
-
- result := ax.WriteFile(ax.Join(binDir, "wails"), []byte(wailsScript), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func setupFakeWails3Toolchain(t *testing.T, binDir string) {
- t.Helper()
-
- wails3Script := `#!/bin/sh
-set -eu
-
-log_file="${WAILS_BUILD_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$@" > "$log_file"
- printf '%s\n' "GOFLAGS=${GOFLAGS:-}" >> "$log_file"
- if [ -n "${GOCACHE:-}" ]; then
- printf '%s\n' "GOCACHE=${GOCACHE}" >> "$log_file"
- fi
- if [ -n "${GOMODCACHE:-}" ]; then
- printf '%s\n' "GOMODCACHE=${GOMODCACHE}" >> "$log_file"
- fi
-fi
-
-sequence_file="${BUILD_SEQUENCE_FILE:-}"
-if [ -n "$sequence_file" ]; then
- printf '%s\n' "wails3" >> "$sequence_file"
- printf '%s\n' "$@" >> "$sequence_file"
- if [ -n "${GOFLAGS:-}" ]; then
- printf '%s\n' "GOFLAGS=${GOFLAGS}" >> "$sequence_file"
- fi
-fi
-
-verb="${1:-build}"
-shift || true
-
-goos=""
-goarch=""
-for arg in "$@"; do
- case "$arg" in
- GOOS=*) goos="${arg#GOOS=}" ;;
- GOARCH=*) goarch="${arg#GOARCH=}" ;;
- esac
-done
-
- name="${NAME:-testapp}"
- if [ "$verb" = "package" ] && [ "$goos" = "windows" ]; then
- mkdir -p "build/windows/nsis"
- printf 'fake wails3 installer\n' > "build/windows/nsis/${name}-installer.exe"
- chmod +x "build/windows/nsis/${name}-installer.exe"
- exit 0
- fi
-
- mkdir -p "bin"
- if [ "$goos" = "windows" ]; then
- name="${name}.exe"
- fi
- printf 'fake wails3 binary\n' > "bin/${name}"
- chmod +x "bin/${name}"
-`
- result := ax.WriteFile(ax.Join(binDir, "wails3"), []byte(wails3Script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func setupFakeWails3GoBuildToolchain(t *testing.T, binDir string) {
- t.Helper()
-
- wails3Script := `#!/bin/sh
-set -eu
-
-name="${NAME:-testapp}"
-mkdir -p "bin"
-go build -o "bin/${name}" .
-`
- result := ax.WriteFile(ax.Join(binDir, "wails3"), []byte(wails3Script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- garbleScript := `#!/bin/sh
-set -eu
-
-log_file="${GARBLE_LOG_FILE:-}"
-if [ -n "$log_file" ]; then
- printf '%s\n' "$@" > "$log_file"
-fi
-
-output=""
-while [ "$#" -gt 0 ]; do
- case "$1" in
- -o)
- shift
- output="${1:-}"
- ;;
- esac
- shift || true
-done
-
-if [ -z "$output" ]; then
- echo "missing -o output path" >&2
- exit 1
-fi
-
-mkdir -p "$(dirname "$output")"
-printf 'fake garbled binary\n' > "$output"
-chmod +x "$output"
-`
- result = ax.WriteFile(ax.Join(binDir, "garble"), []byte(garbleScript), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func setupFakeFrontendCommand(t *testing.T, binDir, name string) {
- t.Helper()
-
- script := core.Replace(`#!/bin/sh
-set -eu
-
-sequence_file="${BUILD_SEQUENCE_FILE:-}"
-if [ -n "$sequence_file" ]; then
- printf '%s\n' "__NAME__" >> "$sequence_file"
- printf '%s\n' "$@" >> "$sequence_file"
- if [ -n "${CUSTOM_ENV:-}" ]; then
- printf '%s\n' "CUSTOM_ENV=${CUSTOM_ENV}" >> "$sequence_file"
- fi
-fi
-`, "__NAME__", name)
- result := ax.WriteFile(ax.Join(binDir, name), []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
-}
-
-func assertWailsLogLines(t *testing.T, logPath string, want ...string) []string {
- t.Helper()
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- lines := core.Split(core.Trim(string(content)), "\n")
- if !stdlibAssertEqual(want, lines) {
- t.Fatalf("want %v, got %v", want, lines)
- }
- return lines
-}
-
-func assertWailsPreBuildLog(t *testing.T, cfg *build.Config, logName string, want ...string) {
- t.Helper()
-
- logPath := ax.Join(t.TempDir(), logName)
- t.Setenv("BUILD_SEQUENCE_FILE", logPath)
- result := NewWailsBuilder().PreBuild(context.Background(), cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- assertWailsLogLines(t, logPath, want...)
-}
-
-func assertWailsPackagePreBuildLog(t *testing.T, commands []string, configure func(*build.Config), logName string, want ...string) {
- t.Helper()
-
- binDir := t.TempDir()
- for _, command := range commands {
- setupFakeFrontendCommand(t, binDir, command)
- }
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- result := ax.WriteFile(ax.Join(projectDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- cfg := &build.Config{FS: storage.Local, ProjectDir: projectDir}
- if configure != nil {
- configure(cfg)
- }
- assertWailsPreBuildLog(t, cfg, logName, want...)
-}
-
-func TestWails_WailsBuilderBuildTaskfileGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- // Check if task is available
- if result := ax.LookPath("task"); !result.OK {
- t.Skip("task not installed, skipping test")
- }
-
- t.Run("delegates to Taskfile if present", func(t *testing.T) {
- fs := storage.Local
- projectDir := setupWailsTestProject(t)
- outputDir := t.TempDir()
-
- // Create a Taskfile that just touches a file
- taskfile := `version: '3'
-tasks:
- build:
- cmds:
- - mkdir -p {{.OUTPUT_DIR}}/{{.GOOS}}_{{.GOARCH}}
- - touch {{.OUTPUT_DIR}}/{{.GOOS}}_{{.GOARCH}}/testapp
-`
- result := ax.WriteFile(ax.Join(projectDir, "Taskfile.yml"), []byte(taskfile), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: fs,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testapp",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if stdlibAssertEmpty(artifacts) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("passes Wails v3 build vars through Taskfile builds", func(t *testing.T) {
- projectDir := setupWailsTestProject(t)
- outputDir := t.TempDir()
- binDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "task.env")
- taskPath := ax.Join(binDir, "task")
-
- script := `#!/bin/sh
-set -eu
-
-env | sort > "${TASK_BUILD_LOG_FILE}"
-
-name="${NAME:-testapp}"
-if [ "${GOOS:-}" = "windows" ]; then
- name="${name}.exe"
-fi
-
-mkdir -p "${OUTPUT_DIR}/${GOOS}_${GOARCH}"
-printf 'taskfile build\n' > "${OUTPUT_DIR}/${GOOS}_${GOARCH}/${name}"
-chmod +x "${OUTPUT_DIR}/${GOOS}_${GOARCH}/${name}"
-`
- result := ax.WriteFile(taskPath, []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("TASK_BUILD_LOG_FILE", logPath)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testapp",
- Version: "v1.2.3",
- BuildTags: []string{"integration"},
- LDFlags: []string{"-s", "-w"},
- WebView2: "download",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "windows", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "GOOS=windows") {
- t.Fatalf("expected %v to contain %v", string(content), "GOOS=windows")
- }
- if !stdlibAssertContains(string(content), "GOARCH=amd64") {
- t.Fatalf("expected %v to contain %v", string(content), "GOARCH=amd64")
- }
- if !stdlibAssertContains(string(content), "CGO_ENABLED=1") {
- t.Fatalf("expected %v to contain %v", string(content), "CGO_ENABLED=1")
- }
- if !stdlibAssertContains(string(content), "GOFLAGS=-trimpath -tags=integration -ldflags=-s -w -X main.version=v1.2.3") {
- t.Fatalf("expected %v to contain %v", string(content), "GOFLAGS=-trimpath -tags=integration -ldflags=-s -w -X main.version=v1.2.3")
- }
- if !stdlibAssertContains(string(content), "EXTRA_TAGS=integration") {
- t.Fatalf("expected %v to contain %v", string(content), "EXTRA_TAGS=integration")
- }
- if !stdlibAssertContains(string(content), "WEBVIEW2_MODE=download") {
- t.Fatalf("expected %v to contain %v", string(content), "WEBVIEW2_MODE=download")
- }
- if !stdlibAssertContains(string(content), `BUILD_FLAGS=-tags production,integration -trimpath -buildvcs=false -ldflags="-s -w -H windowsgui -X main.version=v1.2.3"`) {
- t.Fatalf("expected %v to contain %v", string(content), `BUILD_FLAGS=-tags production,integration -trimpath -buildvcs=false -ldflags="-s -w -H windowsgui -X main.version=v1.2.3"`)
- }
-
- })
-
- t.Run("uses the garble shim for Wails v3 Taskfile builds", func(t *testing.T) {
- projectDir := setupWailsTestProject(t)
- binDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "garble.log")
- taskPath := ax.Join(binDir, "task")
-
- script := `#!/bin/sh
-set -eu
-
-name="${NAME:-testapp}"
-mkdir -p "${OUTPUT_DIR}/${GOOS}_${GOARCH}"
-go build -o "${OUTPUT_DIR}/${GOOS}_${GOARCH}/${name}" .
-`
- result := ax.WriteFile(taskPath, []byte(script), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- setupFakeWails3GoBuildToolchain(t, binDir)
- t.Setenv("GARBLE_LOG_FILE", logPath)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "testapp",
- Obfuscate: true,
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "build") {
- t.Fatalf("expected %v to contain %v", string(content), "build")
- }
- if !stdlibAssertContains(string(content), "-o") {
- t.Fatalf("expected %v to contain %v", string(content), "-o")
- }
-
- })
-}
-
-func TestWails_WailsBuilderNameGood(t *testing.T) {
- builder := NewWailsBuilder()
- if !stdlibAssertEqual("wails", builder.Name()) {
- t.Fatalf("want %v, got %v", "wails", builder.Name())
- }
-
-}
-
-func TestWails_WailsBuilderBuildV3ConfigGood(t *testing.T) {
- builder := NewWailsBuilder()
- cfg := &build.Config{
- CGO: false,
- Name: "testapp",
- Flags: []string{"-trimpath"},
- LDFlags: []string{
- "-s",
- "-w",
- },
- }
-
- v3Config := builder.buildV3Config(cfg)
- if stdlibAssertNil(v3Config) {
- t.Fatal("expected non-nil")
- }
- if cfg.CGO {
- t.Fatal("expected false")
- }
- if !(v3Config.CGO) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(cfg.Name, v3Config.Name) {
- t.Fatalf("want %v, got %v", cfg.Name, v3Config.Name)
- }
- if !stdlibAssertEqual(cfg.Flags, v3Config.Flags) {
- t.Fatalf("want %v, got %v", cfg.Flags, v3Config.Flags)
- }
- if !stdlibAssertEqual(cfg.LDFlags, v3Config.LDFlags) {
- t.Fatalf("want %v, got %v", cfg.LDFlags, v3Config.LDFlags)
- }
-
-}
-
-func TestWails_WailsBuilderResolveFrontendDirGood(t *testing.T) {
- builder := NewWailsBuilder()
- fs := storage.Local
-
- for _, tc := range []struct {
- name string
- frontend []string
- marker string
- denoEnable bool
- wantEmpty bool
- }{
- {name: "finds nested package.json frontends", frontend: []string{"apps", "web"}, marker: "package.json"},
- {name: "finds nested deno.json frontends", frontend: []string{"packages", "site"}, marker: "deno.json"},
- {name: "ignores frontends deeper than depth 2", frontend: []string{"apps", "marketing", "web"}, marker: "package.json", wantEmpty: true},
- {name: "falls back to frontend directory when DENO_ENABLE is set", frontend: []string{"frontend"}, denoEnable: true},
- } {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- if tc.denoEnable {
- t.Setenv("DENO_ENABLE", "true")
- }
-
- projectDir := t.TempDir()
- frontendDir := ax.Join(append([]string{projectDir}, tc.frontend...)...)
- result := ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if tc.marker != "" {
- result = ax.WriteFile(ax.Join(frontendDir, tc.marker), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- }
-
- got := builder.resolveFrontendDir(fs, projectDir)
- if tc.wantEmpty {
- if !stdlibAssertEmpty(got) {
- t.Fatalf("expected empty, got %v", got)
- }
- return
- }
- if !stdlibAssertEqual(frontendDir, got) {
- t.Fatalf("want %v, got %v", frontendDir, got)
- }
- })
- }
-}
-
-func TestWails_WailsBuilderBuildV2Good(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeWailsToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- builder := NewWailsBuilder()
-
- t.Run("builds v2 project", func(t *testing.T) {
- fs := storage.Local
- projectDir := setupWailsV2TestProject(t)
- outputDir := t.TempDir()
-
- cfg := &build.Config{
- FS: fs,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testapp",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !(storage.Local.Exists(artifacts[0].Path)) {
- t.Fatal("expected true")
- }
-
- })
-}
-
-func TestWails_copyBuildArtifact_PreservesMode_Good(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("executable mode bits are not portable on Windows")
- }
-
- sourceDir := t.TempDir()
- sourcePath := ax.Join(sourceDir, "testapp")
- result := ax.WriteFile(sourcePath, []byte("fake wails binary\n"), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- destDir := t.TempDir()
- destPath := ax.Join(destDir, "testapp")
- result = copyBuildArtifact(storage.Local, sourcePath, destPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- stat := ax.Stat(destPath)
- if !stat.OK {
- t.Fatalf("unexpected error: %v", stat.Error())
- }
- info := stat.Value.(stdfs.FileInfo)
- if stdlibAssertZero(info.Mode() & 0o111) {
- t.Fatal("expected non-zero")
- }
-
-}
-
-func TestWails_WailsBuilderBuildV2FlagsGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeWailsToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsV2TestProject(t)
- outputDir := t.TempDir()
- logDir := t.TempDir()
- logPath := ax.Join(logDir, "wails.log")
- t.Setenv("WAILS_BUILD_LOG_FILE", logPath)
-
- goCacheDir := ax.Join(outputDir, "cache", "go-build")
- goModCacheDir := ax.Join(outputDir, "cache", "go-mod")
-
- builder := NewWailsBuilder()
- t.Run("includes Windows-only packaging flags for Windows targets", func(t *testing.T) {
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testapp",
- Version: "v1.2.3",
- BuildTags: []string{"integration", "webkit2_41"},
- LDFlags: []string{"-s", "-w"},
- Obfuscate: true,
- NSIS: true,
- WebView2: "embed",
- Cache: build.CacheConfig{
- Enabled: true,
- Paths: []string{
- goCacheDir,
- goModCacheDir,
- },
- },
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "windows", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- args := core.Split(core.Trim(string(content)), "\n")
- if stdlibAssertEmpty(args) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertEqual("build", args[0]) {
- t.Fatalf("want %v, got %v", "build", args[0])
- }
- if !stdlibAssertContains(args, "-o") {
- t.Fatalf("expected %v to contain %v", args, "-o")
- }
- if !stdlibAssertContains(args, "testapp") {
- t.Fatalf("expected %v to contain %v", args, "testapp")
- }
- if !stdlibAssertContains(args, "-tags") {
- t.Fatalf("expected %v to contain %v", args, "-tags")
- }
- if !stdlibAssertContains(args, "integration,webkit2_41") {
- t.Fatalf("expected %v to contain %v", args, "integration,webkit2_41")
- }
- if !stdlibAssertContains(args, "-ldflags") {
- t.Fatalf("expected %v to contain %v", args, "-ldflags")
- }
- if !stdlibAssertContains(args, "-s -w -X main.version=v1.2.3") {
- t.Fatalf("expected %v to contain %v", args, "-s -w -X main.version=v1.2.3")
- }
- if !stdlibAssertContains(args, "-obfuscated") {
- t.Fatalf("expected %v to contain %v", args, "-obfuscated")
- }
- if !stdlibAssertContains(args, "-nsis") {
- t.Fatalf("expected %v to contain %v", args, "-nsis")
- }
- if !stdlibAssertContains(args, "-webview2") {
- t.Fatalf("expected %v to contain %v", args, "-webview2")
- }
- if !stdlibAssertContains(args, "embed") {
- t.Fatalf("expected %v to contain %v", args, "embed")
- }
- if !stdlibAssertContains(args, "GOCACHE="+goCacheDir) {
- t.Fatalf("expected %v to contain %v", args, "GOCACHE="+goCacheDir)
- }
- if !stdlibAssertContains(args, "GOMODCACHE="+goModCacheDir) {
- t.Fatalf("expected %v to contain %v", args, "GOMODCACHE="+goModCacheDir)
- }
-
- })
-
- t.Run("omits Windows-only packaging flags for non-Windows targets", func(t *testing.T) {
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testapp",
- Version: "v1.2.3",
- BuildTags: []string{"integration", "webkit2_41"},
- LDFlags: []string{"-s", "-w"},
- Obfuscate: true,
- NSIS: true,
- WebView2: "embed",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- args := core.Split(core.Trim(string(content)), "\n")
- if stdlibAssertEmpty(args) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertContains(args, "-o") {
- t.Fatalf("expected %v to contain %v", args, "-o")
- }
- if !stdlibAssertContains(args, "testapp") {
- t.Fatalf("expected %v to contain %v", args, "testapp")
- }
- if stdlibAssertContains(args, "-nsis") {
- t.Fatalf("expected %v not to contain %v", args, "-nsis")
- }
- if stdlibAssertContains(args, "-webview2") {
- t.Fatalf("expected %v not to contain %v", args, "-webview2")
- }
- if stdlibAssertContains(args, "embed") {
- t.Fatalf("expected %v not to contain %v", args, "embed")
- }
-
- })
-}
-
-func TestWails_WailsBuilderBuildV2_RespectsConfiguredOutputNameGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- cases := []struct {
- name string
- target build.Target
- nsis bool
- expectedBase string
- }{
- {
- name: "linux binary",
- target: build.Target{OS: "linux", Arch: "amd64"},
- expectedBase: "customapp",
- },
- {
- name: "darwin app bundle",
- target: build.Target{OS: "darwin", Arch: "arm64"},
- expectedBase: "customapp.app",
- },
- {
- name: "windows executable",
- target: build.Target{OS: "windows", Arch: "amd64"},
- expectedBase: "customapp.exe",
- },
- {
- name: "windows nsis installer",
- target: build.Target{OS: "windows", Arch: "amd64"},
- nsis: true,
- expectedBase: "customapp-installer.exe",
- },
- }
-
- for _, tc := range cases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- binDir := t.TempDir()
- setupFakeWailsToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsV2TestProject(t)
- outputDir := t.TempDir()
- logPath := ax.Join(t.TempDir(), "wails.log")
- t.Setenv("WAILS_BUILD_LOG_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "customapp",
- NSIS: tc.nsis,
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{tc.target}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(tc.expectedBase, ax.Base(artifacts[0].Path)) {
- t.Fatalf("want %v, got %v", tc.expectedBase, ax.Base(artifacts[0].Path))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- args := core.Split(core.Trim(string(content)), "\n")
- if !stdlibAssertContains(args, "-o") {
- t.Fatalf("expected %v to contain %v", args, "-o")
- }
- if !stdlibAssertContains(args, "customapp") {
- t.Fatalf("expected %v to contain %v", args, "customapp")
- }
-
- })
- }
-}
-
-func TestWails_WailsBuilderBuildV2FlagsBad(t *testing.T) {
- result := validateWebView2Mode("invalid")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "webview2 must be one of") {
- t.Fatalf("expected error %v to contain %v", result.Error(), "webview2 must be one of")
- }
-
-}
-
-func TestWails_WailsBuilderPreBuildGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- t.Run("uses deno when deno manifest exists", func(t *testing.T) {
- binDir := t.TempDir()
- setupFakeFrontendCommand(t, binDir, "deno")
- setupFakeFrontendCommand(t, binDir, "npm")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- frontendDir := ax.Join(projectDir, "frontend")
- result := ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "frontend.log")
- t.Setenv("BUILD_SEQUENCE_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- }
- result = builder.PreBuild(context.Background(), cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- assertWailsLogLines(t, logPath, "deno", "task", "build")
-
- })
-
- t.Run("uses configured deno build command when provided", func(t *testing.T) {
- binDir := t.TempDir()
- setupFakeFrontendCommand(t, binDir, "deno")
- setupFakeFrontendCommand(t, binDir, "deno-build")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- frontendDir := ax.Join(projectDir, "frontend")
- result := ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "frontend-custom.log")
- t.Setenv("BUILD_SEQUENCE_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- DenoBuild: "deno-build --target release",
- }
- result = builder.PreBuild(context.Background(), cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- assertWailsLogLines(t, logPath, "deno-build", "--target", "release")
-
- })
-
- t.Run("DENO_BUILD env override wins over config", func(t *testing.T) {
- binDir := t.TempDir()
- setupFakeFrontendCommand(t, binDir, "deno")
- setupFakeFrontendCommand(t, binDir, "deno-build")
- setupFakeFrontendCommand(t, binDir, "env-deno-build")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
- t.Setenv("DENO_BUILD", "env-deno-build --env")
-
- projectDir := setupWailsTestProject(t)
- frontendDir := ax.Join(projectDir, "frontend")
- result := ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "frontend-env.log")
- t.Setenv("BUILD_SEQUENCE_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- DenoBuild: "deno-build --config",
- }
- result = builder.PreBuild(context.Background(), cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- assertWailsLogLines(t, logPath, "env-deno-build", "--env")
-
- })
-
- t.Run("falls back to npm when only package.json exists", func(t *testing.T) {
- assertWailsPackagePreBuildLog(t, []string{"deno", "npm"}, nil, "frontend.log", "npm", "run", "build")
- })
-
- t.Run("uses configured npm build command when provided", func(t *testing.T) {
- assertWailsPackagePreBuildLog(t, []string{"npm", "npm-build"}, func(cfg *build.Config) {
- cfg.NpmBuild = "npm-build --scope app"
- }, "frontend-npm-custom.log", "npm-build", "--scope", "app")
- })
-
- t.Run("prefers deno when DENO_ENABLE is set without a deno manifest", func(t *testing.T) {
- t.Setenv("DENO_ENABLE", "true")
-
- assertWailsPackagePreBuildLog(t, []string{"deno", "npm"}, nil, "frontend-deno-enable.log", "deno", "task", "build")
- })
-
- t.Run("uses configured deno build command without a deno manifest", func(t *testing.T) {
- assertWailsPackagePreBuildLog(t, []string{"deno-build", "npm"}, func(cfg *build.Config) {
- cfg.DenoBuild = "deno-build --target release"
- }, "frontend-config-deno.log", "deno-build", "--target", "release")
- })
-
- t.Run("discovers nested package.json in a monorepo", func(t *testing.T) {
- binDir := t.TempDir()
- setupFakeFrontendCommand(t, binDir, "npm")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- frontendDir := ax.Join(projectDir, "apps", "web")
- result := ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "frontend.log")
- t.Setenv("BUILD_SEQUENCE_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- }
- result = builder.PreBuild(context.Background(), cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- assertWailsLogLines(t, logPath, "npm", "run", "build")
-
- })
-
- for _, tc := range []struct {
- name string
- command string
- lock string
- }{
- {name: "uses bun when bun.lockb exists", command: "bun", lock: "bun.lockb"},
- {name: "uses pnpm when pnpm-lock.yaml exists", command: "pnpm", lock: "pnpm-lock.yaml"},
- } {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- binDir := t.TempDir()
- setupFakeFrontendCommand(t, binDir, tc.command)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- frontendDir := ax.Join(projectDir, "frontend")
- result := ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, tc.lock), []byte(""), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- assertWailsPreBuildLog(t, &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- }, "frontend.log", tc.command, "run", "build")
- })
- }
-
- t.Run("uses yarn when yarn.lock exists", func(t *testing.T) {
- binDir := t.TempDir()
- setupFakeFrontendCommand(t, binDir, "yarn")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- frontendDir := ax.Join(projectDir, "frontend")
- result := ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "yarn.lock"), []byte(""), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "frontend.log")
- t.Setenv("BUILD_SEQUENCE_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- }
- result = builder.PreBuild(context.Background(), cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- assertWailsLogLines(t, logPath, "yarn", "build")
-
- })
-}
-
-func TestWails_WailsBuilderBuildV2PreBuildGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeFrontendCommand(t, binDir, "deno")
- setupFakeFrontendCommand(t, binDir, "npm")
- setupFakeWailsToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsV2TestProject(t)
- frontendDir := ax.Join(projectDir, "frontend")
- result := ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- outputDir := t.TempDir()
- sequencePath := ax.Join(t.TempDir(), "build-sequence.log")
- wailsLogPath := ax.Join(t.TempDir(), "wails.log")
- t.Setenv("BUILD_SEQUENCE_FILE", sequencePath)
- t.Setenv("WAILS_BUILD_LOG_FILE", wailsLogPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testapp",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(sequencePath))
-
- lines := core.Split(core.Trim(string(content)), "\n")
- if len(lines) < 4 {
- t.Fatalf("expected %v to be greater than or equal to %v", len(lines), 4)
- }
- if !stdlibAssertEqual("deno", lines[0]) {
- t.Fatalf("want %v, got %v", "deno", lines[0])
- }
- if !stdlibAssertEqual("task", lines[1]) {
- t.Fatalf("want %v, got %v", "task", lines[1])
- }
- if !stdlibAssertEqual("build", lines[2]) {
- t.Fatalf("want %v, got %v", "build", lines[2])
- }
- if !stdlibAssertEqual("wails", lines[3]) {
- t.Fatalf("want %v, got %v", "wails", lines[3])
- }
-
-}
-
-func TestWails_WailsBuilderPropagatesEnvToExternalCommandsGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeFrontendCommand(t, binDir, "deno")
- setupFakeWailsToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsV2TestProject(t)
- frontendDir := ax.Join(projectDir, "frontend")
- result := ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- sequencePath := ax.Join(t.TempDir(), "build-sequence.log")
- t.Setenv("BUILD_SEQUENCE_FILE", sequencePath)
- t.Setenv("CUSTOM_ENV", "expected-value")
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "testapp",
- Env: []string{"CUSTOM_ENV=expected-value"},
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(sequencePath))
-
- lines := core.Split(core.Trim(string(content)), "\n")
- if !stdlibAssertContains(lines, "CUSTOM_ENV=expected-value") {
- t.Fatalf("expected %v to contain %v", lines, "CUSTOM_ENV=expected-value")
- }
-
-}
-
-func TestWails_WailsBuilderResolveWailsCliGood(t *testing.T) {
- builder := NewWailsBuilder()
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "wails")
- result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- command := requireCPPString(t, builder.resolveWailsCli(fallbackPath))
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestWails_WailsBuilderResolveWailsCliBad(t *testing.T) {
- builder := NewWailsBuilder()
- t.Setenv("PATH", "")
-
- result := builder.resolveWailsCli(ax.Join(t.TempDir(), "missing-wails"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "wails CLI not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "wails CLI not found")
- }
-
-}
-
-func TestWails_WailsBuilderDetectGood(t *testing.T) {
- fs := storage.Local
- t.Run("detects Wails project with wails.json", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewWailsBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for Go-only project", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewWailsBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("detects Go project with root frontend package.json", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewWailsBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects Go project with nested frontend deno manifest", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "go.work"), []byte("go 1.26\nuse ."), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- frontendDir := ax.Join(dir, "apps", "web")
- result = ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewWailsBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if !(detected) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for Node.js project", func(t *testing.T) {
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewWailsBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false for empty directory", func(t *testing.T) {
- dir := t.TempDir()
-
- builder := NewWailsBuilder()
- detected := requireCPPBool(t, builder.Detect(fs, dir))
- if detected {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestWails_DetectPackageManagerGood(t *testing.T) {
- fs := storage.Local
- for _, tc := range []struct {
- name string
- files map[string]string
- want string
- }{
- {
- name: "detects declared packageManager value",
- files: map[string]string{
- "package.json": `{"packageManager":"yarn@4.5.1"}`,
- "pnpm-lock.yaml": "",
- },
- want: "yarn",
- },
- {name: "detects bun from bun.lockb", files: map[string]string{"bun.lockb": ""}, want: "bun"},
- {name: "detects bun from bun.lock", files: map[string]string{"bun.lock": ""}, want: "bun"},
- {name: "detects pnpm from pnpm-lock.yaml", files: map[string]string{"pnpm-lock.yaml": ""}, want: "pnpm"},
- {name: "detects yarn from yarn.lock", files: map[string]string{"yarn.lock": ""}, want: "yarn"},
- {name: "detects npm from package-lock.json", files: map[string]string{"package-lock.json": ""}, want: "npm"},
- {name: "defaults to npm when no lock file", want: "npm"},
- {
- name: "prefers bun over other lock files",
- files: map[string]string{
- "bun.lockb": "",
- "yarn.lock": "",
- "package-lock.json": "",
- },
- want: "bun",
- },
- {
- name: "prefers pnpm over yarn and npm",
- files: map[string]string{
- "pnpm-lock.yaml": "",
- "yarn.lock": "",
- "package-lock.json": "",
- },
- want: "pnpm",
- },
- {
- name: "prefers yarn over npm",
- files: map[string]string{
- "yarn.lock": "",
- "package-lock.json": "",
- },
- want: "yarn",
- },
- {name: "normalises package manager version pins", files: map[string]string{"package.json": `{"packageManager":"npm@10.8.2"}`}, want: "npm"},
- } {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- dir := t.TempDir()
- for path, content := range tc.files {
- result := ax.WriteFile(ax.Join(dir, path), []byte(content), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- }
-
- result := detectPackageManager(fs, dir)
- if !stdlibAssertEqual(tc.want, result) {
- t.Fatalf("want %v, got %v", tc.want, result)
- }
- })
- }
-}
-
-func TestWails_CopyBuildArtifactGood(t *testing.T) {
- fs := storage.Local
-
- t.Run("copies files", func(t *testing.T) {
- dir := t.TempDir()
- sourcePath := ax.Join(dir, "build", "bin", "testapp")
- destPath := ax.Join(dir, "dist", "linux_amd64", "testapp")
- result := ax.MkdirAll(ax.Dir(sourcePath), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = fs.Write(sourcePath, "binary-data")
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = copyBuildArtifact(fs, sourcePath, destPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- got := requireCPPString(t, fs.Read(destPath))
- if !stdlibAssertEqual("binary-data", got) {
- t.Fatalf("want %v, got %v", "binary-data", got)
- }
-
- })
-
- t.Run("copies app bundles recursively", func(t *testing.T) {
- dir := t.TempDir()
- sourcePath := ax.Join(dir, "build", "bin", "testapp.app")
- binaryPath := ax.Join(sourcePath, "Contents", "MacOS", "testapp")
- destPath := ax.Join(dir, "dist", "darwin_arm64", "testapp.app")
- result := ax.MkdirAll(ax.Dir(binaryPath), 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = fs.Write(binaryPath, "bundle-binary")
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = copyBuildArtifact(fs, sourcePath, destPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- got := requireCPPString(t, fs.Read(ax.Join(destPath, "Contents", "MacOS", "testapp")))
- if !stdlibAssertEqual("bundle-binary", got) {
- t.Fatalf("want %v, got %v", "bundle-binary", got)
- }
-
- })
-}
-
-func TestWails_WailsBuilderBuildUnsafeVersionBad(t *testing.T) {
- t.Run("returns error for nil config", func(t *testing.T) {
- builder := NewWailsBuilder()
-
- result := builder.Build(context.Background(), nil, []build.Target{{OS: "linux", Arch: "amd64"}})
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "config is nil") {
- t.Fatalf("expected %v to contain %v", result.Error(), "config is nil")
- }
-
- })
-
- t.Run("returns error for empty targets", func(t *testing.T) {
- projectDir := setupWailsTestProject(t)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "test",
- }
-
- result := builder.Build(context.Background(), cfg, []build.Target{})
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "no targets specified") {
- t.Fatalf("expected %v to contain %v", result.Error(), "no targets specified")
- }
-
- })
-}
-
-func TestWails_WailsBuilderBuildGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- // Check if wails3 is available in PATH
- if result := ax.LookPath("wails3"); !result.OK {
- t.Skip("wails3 not installed, skipping integration test")
- }
-
- t.Run("builds for current platform", func(t *testing.T) {
- projectDir := setupWailsTestProject(t)
- outputDir := t.TempDir()
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: "testapp",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, targets))
- if len(artifacts) !=
-
- // Verify artifact properties
- 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- artifact := artifacts[0]
- if !stdlibAssertEqual(runtime.GOOS, artifact.OS) {
- t.Fatalf("want %v, got %v", runtime.GOOS, artifact.OS)
- }
- if !stdlibAssertEqual(runtime.GOARCH, artifact.Arch) {
- t.Fatalf("want %v, got %v", runtime.GOARCH, artifact.Arch)
- }
-
- })
-}
-
-func TestWails_WailsBuilderBuildV3FallbackGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeWails3Toolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- result := ax.RemoveAll(ax.Join(projectDir, "Taskfile.yml"))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "wails3.log")
- t.Setenv("WAILS_BUILD_LOG_FILE", logPath)
-
- builder := NewWailsBuilder()
- goCacheDir := ax.Join(t.TempDir(), "cache", "go-build")
- goModCacheDir := ax.Join(t.TempDir(), "cache", "go-mod")
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "testapp",
- Version: "v1.2.3",
- BuildTags: []string{"integration"},
- LDFlags: []string{"-s", "-w"},
- Cache: build.CacheConfig{
- Enabled: true,
- Paths: []string{
- goCacheDir,
- goModCacheDir,
- },
- },
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
- if !stdlibAssertEqual("testapp", ax.Base(artifacts[0].Path)) {
- t.Fatalf("want %v, got %v", "testapp", ax.Base(artifacts[0].Path))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- lines := core.Split(core.Trim(string(content)), "\n")
- if len(lines) < 4 {
- t.Fatalf("expected %v to be greater than or equal to %v", len(lines), 4)
- }
- if !stdlibAssertEqual("build", lines[0]) {
- t.Fatalf("want %v, got %v", "build", lines[0])
- }
- if !stdlibAssertContains(lines, "GOOS=linux") {
- t.Fatalf("expected %v to contain %v", lines, "GOOS=linux")
- }
- if !stdlibAssertContains(lines, "GOARCH=amd64") {
- t.Fatalf("expected %v to contain %v", lines, "GOARCH=amd64")
- }
- if !stdlibAssertContains(lines, "EXTRA_TAGS=integration") {
- t.Fatalf("expected %v to contain %v", lines, "EXTRA_TAGS=integration")
- }
- joinedLines := core.Join("\n", lines...)
- if !stdlibAssertContains(joinedLines, `BUILD_FLAGS=-tags production,integration -trimpath -buildvcs=false -ldflags="-s -w -X main.version=v1.2.3"`) {
- t.Fatalf("expected %v to contain %v", joinedLines, `BUILD_FLAGS=-tags production,integration -trimpath -buildvcs=false -ldflags="-s -w -X main.version=v1.2.3"`)
- }
- if !stdlibAssertContains(joinedLines, "GOFLAGS=-trimpath -tags=integration -ldflags=-s -w -X main.version=v1.2.3") {
- t.Fatalf("expected %v to contain %v", joinedLines, "GOFLAGS=-trimpath -tags=integration -ldflags=-s -w -X main.version=v1.2.3")
- }
- if !stdlibAssertContains(lines, "GOCACHE="+goCacheDir) {
- t.Fatalf("expected %v to contain %v", lines, "GOCACHE="+goCacheDir)
- }
- if !stdlibAssertContains(lines, "GOMODCACHE="+goModCacheDir) {
- t.Fatalf("expected %v to contain %v", lines, "GOMODCACHE="+goModCacheDir)
- }
-
-}
-
-func TestWails_WailsBuilderBuildV3Fallback_Obfuscate_Good(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeWails3GoBuildToolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- result := ax.RemoveAll(ax.Join(projectDir, "Taskfile.yml"))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "garble.log")
- t.Setenv("GARBLE_LOG_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "testapp",
- Obfuscate: true,
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- lines := core.Split(core.Trim(string(content)), "\n")
- if len(lines) < 1 {
- t.Fatalf("expected %v to be greater than or equal to %v", len(lines), 1)
- }
- if !stdlibAssertEqual("build", lines[0]) {
- t.Fatalf("want %v, got %v", "build", lines[0])
- }
- joinedLines := core.Join("\n", lines...)
- if !stdlibAssertContains(joinedLines, "-o") {
- t.Fatalf("expected %v to contain %v", joinedLines, "-o")
- }
-
-}
-
-func TestWails_WailsBuilderBuildV3Fallback_PreBuildGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeWails3Toolchain(t, binDir)
- setupFakeFrontendCommand(t, binDir, "deno")
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- result := ax.RemoveAll(ax.Join(projectDir, "Taskfile.yml"))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- frontendDir := ax.Join(projectDir, "frontend")
- result = ax.MkdirAll(frontendDir, 0o755)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- result = ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte(`{}`), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "build-sequence.log")
- t.Setenv("BUILD_SEQUENCE_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "testapp",
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- lines := core.Split(core.Trim(string(content)), "\n")
- if len(lines) < 7 {
- t.Fatalf("expected %v to be greater than or equal to %v", len(lines), 7)
- }
- if !stdlibAssertEqual("deno", lines[0]) {
- t.Fatalf("want %v, got %v", "deno", lines[0])
- }
- if !stdlibAssertEqual("task", lines[1]) {
- t.Fatalf("want %v, got %v", "task", lines[1])
- }
- if !stdlibAssertEqual("build", lines[2]) {
- t.Fatalf("want %v, got %v", "build", lines[2])
- }
- if !stdlibAssertEqual("wails3", lines[3]) {
- t.Fatalf("want %v, got %v", "wails3", lines[3])
- }
- if !stdlibAssertEqual("build", lines[4]) {
- t.Fatalf("want %v, got %v", "build", lines[4])
- }
- if !stdlibAssertContains(lines, "GOOS=linux") {
- t.Fatalf("expected %v to contain %v", lines, "GOOS=linux")
- }
- if !stdlibAssertContains(lines, "GOARCH=amd64") {
- t.Fatalf("expected %v to contain %v", lines, "GOARCH=amd64")
- }
-
-}
-
-func TestWails_WailsBuilderBuildV3NSISGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- binDir := t.TempDir()
- setupFakeWails3Toolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- result := ax.RemoveAll(ax.Join(projectDir, "Taskfile.yml"))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "wails3-package.log")
- t.Setenv("WAILS_BUILD_LOG_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "testapp",
- NSIS: true,
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "windows", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
- if !stdlibAssertEqual("testapp-installer.exe", ax.Base(artifacts[0].Path)) {
- t.Fatalf("want %v, got %v", "testapp-installer.exe", ax.Base(artifacts[0].Path))
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
-
- lines := core.Split(core.Trim(string(content)), "\n")
- if len(lines) < 3 {
- t.Fatalf("expected %v to be greater than or equal to %v", len(lines), 3)
- }
- if !stdlibAssertEqual("package", lines[0]) {
- t.Fatalf("want %v, got %v", "package", lines[0])
- }
- if !stdlibAssertContains(lines, "GOOS=windows") {
- t.Fatalf("expected %v to contain %v", lines, "GOOS=windows")
- }
- if !stdlibAssertContains(lines, "GOARCH=amd64") {
- t.Fatalf("expected %v to contain %v", lines, "GOARCH=amd64")
- }
-
-}
-
-func TestWails_WailsBuilderBuildV3NSISWebView2DownloadGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- assertWailsBuilderBuildV3NSISWebView2(t, "download")
-}
-
-func assertWailsBuilderBuildV3NSISWebView2(t *testing.T, mode string) {
- t.Helper()
-
- binDir := t.TempDir()
- setupFakeWails3Toolchain(t, binDir)
- t.Setenv("PATH", binDir+string(core.PathListSeparator)+core.Getenv("PATH"))
-
- projectDir := setupWailsTestProject(t)
- result := ax.RemoveAll(ax.Join(projectDir, "Taskfile.yml"))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- logPath := ax.Join(t.TempDir(), "wails3-package-webview2.log")
- t.Setenv("WAILS_BUILD_LOG_FILE", logPath)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "testapp",
- NSIS: true,
- WebView2: mode,
- }
-
- artifacts := requireCPPArtifacts(t, builder.Build(context.Background(), cfg, []build.Target{{OS: "windows", Arch: "amd64"}}))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if stat := ax.Stat(artifacts[0].Path); !stat.OK {
- t.Fatalf("expected file to exist: %v", artifacts[0].Path)
- }
-
- content := requireBuilderBytes(t, ax.ReadFile(logPath))
- if !stdlibAssertContains(string(content), "WEBVIEW2_MODE="+mode) {
- t.Fatalf("expected %v to contain %v", string(content), "WEBVIEW2_MODE="+mode)
- }
-}
-
-func TestWails_buildV3TaskVars_WebView2Modes_Good(t *testing.T) {
- modes := []string{"download", "embed", "browser", "error"}
- for _, mode := range modes {
- t.Run(mode, func(t *testing.T) {
- result := buildV3TaskVars(&build.Config{WebView2: mode}, build.Target{OS: "windows", Arch: "amd64"})
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- taskVars := result.Value.([]string)
- if !stdlibAssertContains(taskVars, "WEBVIEW2_MODE="+mode) {
- t.Fatalf("expected %v to contain %v", taskVars, "WEBVIEW2_MODE="+mode)
- }
-
- })
- }
-}
-
-func TestWails_WailsBuilderBuildV3NSISWebView2EmbedGood(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- assertWailsBuilderBuildV3NSISWebView2(t, "embed")
-}
-
-func TestWails_WailsBuilderBuildBad(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- projectDir := setupWailsTestProject(t)
- result := ax.RemoveAll(ax.Join(projectDir, "Taskfile.yml"))
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "unsafe-version",
- Version: "v1.2.3 && echo unsafe",
- }
-
- result = builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "unsupported characters") {
-
- // Verify WailsBuilder implements Builder interface
- t.Fatalf("expected %v to contain %v", result.Error(), "unsupported characters")
- }
-
-}
-
-func TestWails_WailsBuilderInterfaceGood(t *testing.T) {
- builder := NewWailsBuilder()
- var _ build.Builder = builder
- if !stdlibAssertEqual("wails", builder.Name()) {
- t.Fatalf("want %v, got %v", "wails", builder.Name())
- }
- detected := requireCPPBool(t, builder.Detect(nil, t.TempDir()))
- if detected {
- t.Fatal("expected empty temp directory not to be detected")
- }
-}
-
-func TestWails_WailsBuilderUgly(t *testing.T) {
- t.Run("handles nonexistent frontend directory gracefully", func(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- // Create a Wails project without a frontend directory
- dir := t.TempDir()
- result := ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: dir,
- OutputDir: t.TempDir(),
- Name: "test",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- // This will fail because wails3 isn't set up, but it shouldn't panic
- // due to missing frontend directory
- result = builder.Build(context.Background(), cfg, targets)
- // We expect an error (wails3 build will fail), but not a panic
- // The error should be about wails3 build, not about frontend
- if !result.OK {
- if stdlibAssertContains(result.Error(), "frontend dependencies") {
- t.Fatalf("expected %v not to contain %v", result.Error(), "frontend dependencies")
- }
-
- }
- })
-
- t.Run("handles context cancellation", func(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test in short mode")
- }
-
- projectDir := setupWailsTestProject(t)
-
- builder := NewWailsBuilder()
- cfg := &build.Config{
- FS: storage.Local,
- ProjectDir: projectDir,
- OutputDir: t.TempDir(),
- Name: "canceltest",
- }
- targets := []build.Target{
- {OS: runtime.GOOS, Arch: runtime.GOARCH},
- }
-
- // Create an already cancelled context
- ctx, cancel := context.WithCancel(context.Background())
- cancel()
-
- result := builder.Build(ctx, cfg, targets)
- if result.OK {
- t.Fatal("expected error")
- }
-
- })
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestWails_NewWailsBuilder_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewWailsBuilder()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestWails_NewWailsBuilder_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewWailsBuilder()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWails_NewWailsBuilder_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewWailsBuilder()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWails_WailsBuilder_Name_Good(t *core.T) {
- subject := &WailsBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestWails_WailsBuilder_Name_Bad(t *core.T) {
- subject := &WailsBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWails_WailsBuilder_Name_Ugly(t *core.T) {
- subject := &WailsBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWails_WailsBuilder_Detect_Good(t *core.T) {
- subject := &WailsBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestWails_WailsBuilder_Detect_Bad(t *core.T) {
- subject := &WailsBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWails_WailsBuilder_Detect_Ugly(t *core.T) {
- subject := &WailsBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Detect(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWails_WailsBuilder_Build_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &WailsBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestWails_WailsBuilder_Build_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &WailsBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWails_WailsBuilder_Build_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &WailsBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Build(ctx, nil, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWails_WailsBuilder_PreBuild_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &WailsBuilder{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.PreBuild(ctx, nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestWails_WailsBuilder_PreBuild_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &WailsBuilder{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.PreBuild(ctx, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWails_WailsBuilder_PreBuild_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &WailsBuilder{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.PreBuild(ctx, nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/builders/zip_deterministic.go b/pkg/build/builders/zip_deterministic.go
deleted file mode 100644
index faa0e53..0000000
--- a/pkg/build/builders/zip_deterministic.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package builders
-
-import "time"
-
-var deterministicZipTime = time.Unix(0, 0).UTC()
diff --git a/pkg/build/builtin_resolver.go b/pkg/build/builtin_resolver.go
deleted file mode 100644
index 09f1fda..0000000
--- a/pkg/build/builtin_resolver.go
+++ /dev/null
@@ -1,228 +0,0 @@
-package build
-
-import (
- "context"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func resolveBuiltinBuilder(projectType ProjectType) core.Result {
- switch projectType {
- case ProjectTypeGo:
- return core.Ok(&builtinGoBuilder{})
- default:
- return core.Fail(core.E(
- "build.resolveBuiltinBuilder",
- "no builder resolver registered; builtin fallback only supports go projects (requested "+string(projectType)+")",
- nil,
- ))
- }
-}
-
-type builtinGoBuilder struct{}
-
-func (b *builtinGoBuilder) Name() string { return "go" }
-
-func (b *builtinGoBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(IsGoProject(fs, dir))
-}
-
-func (b *builtinGoBuilder) Build(ctx context.Context, cfg *Config, targets []Target) core.Result {
- if cfg == nil {
- return core.Fail(core.E("builtinGoBuilder.Build", "config is nil", nil))
- }
-
- if len(targets) == 0 {
- targets = []Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
- }
-
- filesystem := cfg.FS
- if filesystem == nil {
- filesystem = storage.Local
- }
-
- outputDir := cfg.OutputDir
- if outputDir == "" {
- outputDir = ax.Join(cfg.ProjectDir, "dist")
- }
- created := filesystem.EnsureDir(outputDir)
- if !created.OK {
- return core.Fail(core.E("builtinGoBuilder.Build", "failed to create output directory", core.NewError(created.Error())))
- }
-
- artifacts := make([]Artifact, 0, len(targets))
- for _, target := range targets {
- artifactResult := b.buildTarget(ctx, filesystem, cfg, outputDir, target)
- if !artifactResult.OK {
- return core.Fail(core.E("builtinGoBuilder.Build", "failed to build "+target.String(), core.NewError(artifactResult.Error())))
- }
- artifacts = append(artifacts, artifactResult.Value.(Artifact))
- }
-
- return core.Ok(artifacts)
-}
-
-func (b *builtinGoBuilder) buildTarget(ctx context.Context, filesystem storage.Medium, cfg *Config, outputDir string, target Target) core.Result {
- binaryName := cfg.Name
- if binaryName == "" {
- binaryName = cfg.Project.Binary
- }
- if binaryName == "" {
- binaryName = cfg.Project.Name
- }
- if binaryName == "" {
- binaryName = ax.Base(cfg.ProjectDir)
- }
-
- if target.OS == "windows" && !core.HasSuffix(binaryName, ".exe") {
- binaryName += ".exe"
- }
-
- platformDir := ax.Join(outputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
- created := filesystem.EnsureDir(platformDir)
- if !created.OK {
- return core.Fail(core.E("builtinGoBuilder.buildTarget", "failed to create platform directory", core.NewError(created.Error())))
- }
-
- outputPath := ax.Join(platformDir, binaryName)
-
- args := []string{"build"}
- if !builtinContainsString(cfg.Flags, "-trimpath") {
- args = append(args, "-trimpath")
- }
- if len(cfg.Flags) > 0 {
- args = append(args, cfg.Flags...)
- }
- if len(cfg.BuildTags) > 0 {
- args = append(args, "-tags", core.Join(",", cfg.BuildTags...))
- }
-
- ldflags := append([]string{}, cfg.LDFlags...)
- if cfg.Version != "" && !builtinHasVersionLDFlag(ldflags) {
- versionFlag := VersionLinkerFlag(cfg.Version)
- if !versionFlag.OK {
- return versionFlag
- }
- ldflags = append(ldflags, versionFlag.Value.(string))
- }
- if len(ldflags) > 0 {
- args = append(args, "-ldflags", core.Join(" ", ldflags...))
- }
-
- args = append(args, "-o", outputPath)
-
- mainPackage := cfg.Project.Main
- if mainPackage == "" {
- mainPackage = "."
- }
- args = append(args, mainPackage)
-
- env := append([]string{}, cfg.Env...)
- env = append(env, CacheEnvironment(&cfg.Cache)...)
- env = append(env,
- core.Sprintf("TARGET_OS=%s", target.OS),
- core.Sprintf("TARGET_ARCH=%s", target.Arch),
- core.Sprintf("OUTPUT_DIR=%s", outputDir),
- core.Sprintf("TARGET_DIR=%s", platformDir),
- core.Sprintf("GOOS=%s", target.OS),
- core.Sprintf("GOARCH=%s", target.Arch),
- )
- if binaryName != "" {
- env = append(env, core.Sprintf("NAME=%s", binaryName))
- }
- if cfg.Version != "" {
- env = append(env, core.Sprintf("VERSION=%s", cfg.Version))
- }
- if cfg.CGO {
- env = append(env, "CGO_ENABLED=1")
- } else {
- env = append(env, "CGO_ENABLED=0")
- }
-
- command := "go"
- if cfg.Obfuscate {
- resolved := resolveBuiltinGarbleCli()
- if !resolved.OK {
- return resolved
- }
- command = resolved.Value.(string)
- }
-
- output := ax.CombinedOutput(ctx, cfg.ProjectDir, env, command, args...)
- if !output.OK {
- return core.Fail(core.E("builtinGoBuilder.buildTarget", command+" build failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(Artifact{
- Path: outputPath,
- OS: target.OS,
- Arch: target.Arch,
- })
-}
-
-func resolveBuiltinGarbleCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/garble",
- "/opt/homebrew/bin/garble",
- }
-
- paths = append(paths, builtinGarbleInstallPaths()...)
-
- if home := core.Env("HOME"); home != "" {
- paths = append(paths, ax.Join(home, "go", "bin", "garble"))
- }
- }
-
- command := ax.ResolveCommand("garble", paths...)
- if !command.OK {
- return core.Fail(core.E("builtinGoBuilder.resolveGarbleCli", "garble CLI not found. Install it with: go install mvdan.cc/garble@latest", core.NewError(command.Error())))
- }
-
- return command
-}
-
-func builtinGarbleInstallPaths() []string {
- var paths []string
-
- if gobin := core.Env("GOBIN"); gobin != "" {
- paths = append(paths, ax.Join(gobin, "garble"))
- }
-
- if gopath := core.Env("GOPATH"); gopath != "" {
- sep := ":"
- if runtime.GOOS == "windows" {
- sep = ";"
- }
- for _, root := range core.Split(gopath, sep) {
- root = core.Trim(root)
- if root == "" {
- continue
- }
- paths = append(paths, ax.Join(root, "bin", "garble"))
- }
- }
-
- return paths
-}
-
-func builtinHasVersionLDFlag(ldflags []string) bool {
- for _, flag := range ldflags {
- if core.Contains(flag, "main.version=") || core.Contains(flag, "main.Version=") {
- return true
- }
- }
- return false
-}
-
-func builtinContainsString(values []string, needle string) bool {
- for _, value := range values {
- if value == needle {
- return true
- }
- }
- return false
-}
diff --git a/pkg/build/builtin_resolver_example_test.go b/pkg/build/builtin_resolver_example_test.go
deleted file mode 100644
index c2bb1df..0000000
--- a/pkg/build/builtin_resolver_example_test.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-type GoBuilder = builtinGoBuilder
-
-// ExampleGoBuilder_Name references GoBuilder.Name on this package API surface.
-func ExampleGoBuilder_Name() {
- _ = (*GoBuilder).Name
- core.Println("GoBuilder.Name")
- // Output: GoBuilder.Name
-}
-
-// ExampleGoBuilder_Detect references GoBuilder.Detect on this package API surface.
-func ExampleGoBuilder_Detect() {
- _ = (*GoBuilder).Detect
- core.Println("GoBuilder.Detect")
- // Output: GoBuilder.Detect
-}
-
-// ExampleGoBuilder_Build references GoBuilder.Build on this package API surface.
-func ExampleGoBuilder_Build() {
- _ = (*GoBuilder).Build
- core.Println("GoBuilder.Build")
- // Output: GoBuilder.Build
-}
diff --git a/pkg/build/builtin_resolver_test.go b/pkg/build/builtin_resolver_test.go
deleted file mode 100644
index 21f16b7..0000000
--- a/pkg/build/builtin_resolver_test.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package build
-
-import (
- "context"
- "runtime"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-func TestBuiltinResolver_GoBuilder_Name_Good(t *core.T) {
- builder := &builtinGoBuilder{}
- name := builder.Name()
- core.AssertEqual(t, "go", name)
- core.AssertNotEmpty(t, name)
-}
-
-func TestBuiltinResolver_GoBuilder_Name_Bad(t *core.T) {
- builder := &builtinGoBuilder{}
- name := builder.Name()
- core.AssertNotEqual(t, "", name)
- core.AssertLen(t, name, 2)
-}
-
-func TestBuiltinResolver_GoBuilder_Name_Ugly(t *core.T) {
- var builder *builtinGoBuilder
- name := builder.Name()
- core.AssertEqual(t, "go", name)
- core.AssertNotEmpty(t, name)
-}
-
-func TestBuiltinResolver_GoBuilder_Detect_Good(t *core.T) {
- dir := t.TempDir()
- writeBuiltinResolverFile(t, ax.Join(dir, "go.mod"), "module example.com/demo\n")
-
- result := (&builtinGoBuilder{}).Detect(coreio.Local, dir)
- core.RequireTrue(t, result.OK)
- detected := result.Value.(bool)
- core.AssertTrue(t, detected)
-}
-
-func TestBuiltinResolver_GoBuilder_Detect_Bad(t *core.T) {
- result := (&builtinGoBuilder{}).Detect(coreio.Local, t.TempDir())
- core.RequireTrue(t, result.OK)
- detected := result.Value.(bool)
- core.AssertFalse(t, detected)
-}
-
-func TestBuiltinResolver_GoBuilder_Detect_Ugly(t *core.T) {
- result := (&builtinGoBuilder{}).Detect(nil, "")
- core.RequireTrue(t, result.OK)
- detected := result.Value.(bool)
- core.AssertFalse(t, detected)
-}
-
-func TestBuiltinResolver_GoBuilder_Build_Good(t *core.T) {
- dir := t.TempDir()
- writeBuiltinResolverFile(t, ax.Join(dir, "go.mod"), "module example.com/demo\n\ngo 1.23\n")
- writeBuiltinResolverFile(t, ax.Join(dir, "main.go"), "package main\n\nfunc main() {}\n")
-
- result := (&builtinGoBuilder{}).Build(context.Background(), &Config{
- FS: coreio.Local,
- ProjectDir: dir,
- OutputDir: ax.Join(dir, "dist"),
- Name: "demo",
- Project: Project{Main: "."},
- }, []Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}})
- core.RequireTrue(t, result.OK)
- artifacts := result.Value.([]Artifact)
- core.AssertLen(t, artifacts, 1)
- core.AssertEqual(t, runtime.GOOS+"/"+runtime.GOARCH, artifacts[0].OS+"/"+artifacts[0].Arch)
-}
-
-func TestBuiltinResolver_GoBuilder_Build_Bad(t *core.T) {
- result := (&builtinGoBuilder{}).Build(context.Background(), nil, nil)
- core.AssertFalse(t, result.OK)
- core.AssertContains(t, result.Error(), "nil")
-}
-
-func TestBuiltinResolver_GoBuilder_Build_Ugly(t *core.T) {
- dir := t.TempDir()
- result := (&builtinGoBuilder{}).Build(context.Background(), &Config{
- FS: coreio.Local,
- ProjectDir: dir,
- OutputDir: ax.Join(dir, "dist"),
- Name: "demo",
- }, []Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}})
- core.AssertFalse(t, result.OK)
-}
-
-func writeBuiltinResolverFile(t *core.T, path, content string) {
- t.Helper()
- core.RequireTrue(t, ax.MkdirAll(ax.Dir(path), 0o755).OK)
- core.RequireTrue(t, ax.WriteFile(path, []byte(content), 0o644).OK)
-}
diff --git a/pkg/build/cache.go b/pkg/build/cache.go
deleted file mode 100644
index 87f673f..0000000
--- a/pkg/build/cache.go
+++ /dev/null
@@ -1,401 +0,0 @@
-// Package build provides project type detection and cross-compilation for the Core build system.
-// This file handles build cache configuration and key generation.
-package build
-
-import (
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
- "gopkg.in/yaml.v3"
-)
-
-// DefaultCacheDirectory is the project-local cache metadata directory used when
-// no cache directory is supplied.
-//
-// cfg := build.CacheConfig{Enabled: true}
-// // SetupCache(storage.Local, ".", &cfg) -> ".core/cache"
-const DefaultCacheDirectory = ".core/cache"
-
-// DefaultProcessCacheDirectory is the RFC-documented cache directory used by
-// the single-argument SetupCache form when only environment wiring is needed.
-const DefaultProcessCacheDirectory = "~/.cache/core-build"
-
-// DefaultBuildCachePaths returns the project-local Go cache directories used
-// when no cache paths are configured.
-//
-// paths := build.DefaultBuildCachePaths("/workspace/project")
-// // ["/workspace/project/cache/go-build", "/workspace/project/cache/go-mod"]
-func DefaultBuildCachePaths(baseDir string) []string {
- if core.Trim(baseDir) == "" {
- return []string{
- "cache/go-build",
- "cache/go-mod",
- }
- }
-
- return []string{
- ax.Join(baseDir, "cache", "go-build"),
- ax.Join(baseDir, "cache", "go-mod"),
- }
-}
-
-// CacheConfig holds build cache configuration loaded from .core/build.yaml.
-//
-// cfg := build.CacheConfig{
-// Enabled: true,
-// Directory: ".core/cache",
-// Paths: []string{"~/.cache/go-build", "~/go/pkg/mod"},
-// }
-type CacheConfig struct {
- // Enabled turns cache setup on for the build.
- Enabled bool `json:"enabled" yaml:"enabled"`
- // Dir is where cache metadata is stored.
- Dir string `json:"dir,omitempty" yaml:"dir,omitempty"`
- // Directory is the deprecated alias for Dir.
- Directory string `json:"-" yaml:"-"`
- // KeyPrefix prefixes the generated cache key.
- KeyPrefix string `json:"key_prefix,omitempty" yaml:"key_prefix,omitempty"`
- // Paths are cache directories that should exist before the build starts.
- Paths []string `json:"paths,omitempty" yaml:"paths,omitempty"`
- // RestoreKeys are fallback prefixes used when the exact cache key is not present.
- RestoreKeys []string `json:"restore_keys,omitempty" yaml:"restore_keys,omitempty"`
-}
-
-// MarshalYAML emits the documented cache configuration shape with the Dir field.
-//
-// data, err := yaml.Marshal(build.CacheConfig{Enabled: true, Dir: ".core/cache"})
-func (c CacheConfig) MarshalYAML() core.Result {
- type rawCacheConfig struct {
- Enabled bool `yaml:"enabled"`
- Dir string `yaml:"dir,omitempty"`
- KeyPrefix string `yaml:"key_prefix,omitempty"`
- Paths []string `yaml:"paths,omitempty"`
- RestoreKeys []string `yaml:"restore_keys,omitempty"`
- }
-
- return core.Ok(rawCacheConfig{
- Enabled: c.Enabled,
- Dir: c.effectiveDirectory(),
- KeyPrefix: c.KeyPrefix,
- Paths: c.Paths,
- RestoreKeys: c.RestoreKeys,
- })
-}
-
-// UnmarshalYAML accepts both the concise build config keys and the longer aliases.
-//
-// err := yaml.Unmarshal([]byte("dir: .core/cache"), &cfg)
-func (c *CacheConfig) UnmarshalYAML(value *yaml.Node) core.Result {
- type rawCacheConfig struct {
- Enabled bool `yaml:"enabled"`
- Directory string `yaml:"directory"`
- Dir string `yaml:"dir"`
- KeyPrefix string `yaml:"key_prefix"`
- Key string `yaml:"key"`
- Paths []string `yaml:"paths"`
- RestoreKeys []string `yaml:"restore_keys"`
- }
-
- var raw rawCacheConfig
- if err := value.Decode(&raw); err != nil {
- return core.Fail(err)
- }
-
- c.Enabled = raw.Enabled
- c.Dir = firstNonEmpty(raw.Dir, raw.Directory)
- c.Directory = c.Dir
- c.KeyPrefix = firstNonEmpty(raw.KeyPrefix, raw.Key)
- c.Paths = raw.Paths
- c.RestoreKeys = raw.RestoreKeys
-
- return core.Ok(nil)
-}
-
-// SetupCache normalises cache paths and ensures the cache directories exist.
-//
-// The canonical form is the 3-argument variant:
-//
-// err := build.SetupCache(storage.Local, ".", &build.CacheConfig{
-// Enabled: true,
-// Paths: []string{"~/.cache/go-build", "~/go/pkg/mod"},
-// })
-//
-// A compatibility 1-argument form is also supported for the RFC-shaped API:
-//
-// err := build.SetupCache(build.CacheConfig{Enabled: true})
-func SetupCache(args ...any) core.Result {
- switch len(args) {
- case 1:
- cfg, ok := cacheConfigArg(args[0])
- if !ok || cfg == nil || !cfg.Enabled {
- return core.Ok(nil)
- }
-
- // The single-argument form configures the process environment for callers
- // that only need cache wiring and do not have a filesystem/project root.
- if cfg.effectiveDirectory() == "" {
- cfg.Dir = DefaultProcessCacheDirectory
- cfg.Directory = DefaultProcessCacheDirectory
- }
- if len(cfg.Paths) == 0 {
- cfg.Paths = []string{"~/.cache/go-build", "~/go/pkg/mod"}
- }
- applyCacheEnvironment(cfg)
- return core.Ok(nil)
- case 3:
- fs, _ := args[0].(storage.Medium)
- dir, _ := args[1].(string)
- cfg, ok := args[2].(*CacheConfig)
- if !ok {
- return core.Fail(core.E("build.SetupCache", "third argument must be *CacheConfig", nil))
- }
- return setupCacheWithMedium(fs, dir, cfg)
- default:
- return core.Fail(core.E("build.SetupCache", "expected 1 or 3 arguments", nil))
- }
-}
-
-func cacheConfigArg(arg any) (*CacheConfig, bool) {
- switch cfg := arg.(type) {
- case CacheConfig:
- return &cfg, true
- case *CacheConfig:
- return cfg, true
- default:
- return nil, false
- }
-}
-
-func setupCacheWithMedium(fs storage.Medium, dir string, cfg *CacheConfig) core.Result {
- if fs == nil || cfg == nil || !cfg.Enabled {
- return core.Ok(nil)
- }
-
- directory := cfg.effectiveDirectory()
- if directory == "" {
- directory = ax.Join(dir, DefaultCacheDirectory)
- }
- directory = normaliseCachePath(dir, directory)
- cfg.Dir = directory
- cfg.Directory = directory
- if len(cfg.Paths) == 0 {
- cfg.Paths = DefaultBuildCachePaths(dir)
- }
-
- created := fs.EnsureDir(directory)
- if !created.OK {
- return core.Fail(core.E("build.SetupCache", "failed to create cache directory", core.NewError(created.Error())))
- }
-
- normalisedPaths := make([]string, 0, len(cfg.Paths))
- for _, path := range cfg.Paths {
- path = normaliseCachePath(dir, path)
- if path == "" {
- continue
- }
- created = fs.EnsureDir(path)
- if !created.OK {
- return core.Fail(core.E("build.SetupCache", "failed to create cache path "+path, core.NewError(created.Error())))
- }
- normalisedPaths = append(normalisedPaths, path)
- }
- cfg.Paths = deduplicateStrings(normalisedPaths)
-
- return core.Ok(nil)
-}
-
-// SetupBuildCache prepares the cache configuration stored on a build config.
-//
-// err := build.SetupBuildCache(storage.Local, ".", cfg)
-func SetupBuildCache(fs storage.Medium, dir string, cfg *BuildConfig) core.Result {
- if fs == nil || cfg == nil {
- return core.Ok(nil)
- }
-
- return setupCacheWithMedium(fs, dir, &cfg.Build.Cache)
-}
-
-// CacheKey returns a deterministic cache key from go.sum, go.work.sum, and the target platform.
-//
-// key := build.CacheKey(storage.Local, ".", "linux", "amd64") // "go-linux-amd64-abc123..."
-func CacheKey(fs storage.Medium, dir, goos, goarch string) string {
- var seed []byte
-
- if fs != nil {
- for _, name := range []string{"go.sum", "go.work.sum"} {
- if content := fs.Read(ax.Join(dir, name)); content.OK {
- seed = append(seed, content.Value.(string)...)
- seed = append(seed, '\n')
- }
- }
- if len(seed) == 0 {
- seed = append(seed, '\n')
- }
- }
-
- seed = append(seed, goos...)
- seed = append(seed, '\n')
- seed = append(seed, goarch...)
-
- suffix := core.SHA256Hex(seed)[:12]
-
- return core.Join("-", "go", goos, goarch, suffix)
-}
-
-// CacheKeyWithConfig returns a deterministic cache key and applies the optional
-// cache key prefix from configuration.
-//
-// key := build.CacheKeyWithConfig(storage.Local, ".", "linux", "amd64", &cfg.Cache)
-// // "demo-go-linux-amd64-abc123..."
-func CacheKeyWithConfig(fs storage.Medium, dir, goos, goarch string, cfg *CacheConfig) string {
- key := CacheKey(fs, dir, goos, goarch)
- if cfg == nil {
- return key
- }
-
- prefix := core.Trim(cfg.KeyPrefix)
- if prefix == "" {
- return key
- }
-
- return core.Join("-", prefix, key)
-}
-
-// CacheRestoreKeys returns the configured restore-key prefixes in stable order.
-//
-// keys := build.CacheRestoreKeys(&build.CacheConfig{
-// KeyPrefix: "demo",
-// RestoreKeys: []string{"go-", "core-"},
-// })
-// // ["demo", "go-", "core-"]
-func CacheRestoreKeys(cfg *CacheConfig) []string {
- if cfg == nil {
- return nil
- }
-
- keys := make([]string, 0, 1+len(cfg.RestoreKeys))
- if prefix := core.Trim(cfg.KeyPrefix); prefix != "" {
- keys = append(keys, prefix)
- }
- keys = append(keys, cfg.RestoreKeys...)
-
- return deduplicateStrings(keys)
-}
-
-// CacheEnvironment returns environment variables derived from the cache config.
-//
-// env := build.CacheEnvironment(&build.CacheConfig{Enabled: true, Paths: []string{"/tmp/go-build"}})
-func CacheEnvironment(cfg *CacheConfig) []string {
- if cfg == nil || !cfg.Enabled {
- return nil
- }
-
- var env []string
-
- for _, path := range cfg.Paths {
- switch cacheEnvironmentName(path) {
- case "GOCACHE":
- env = appendIfMissing(env, "GOCACHE="+path)
- case "GOMODCACHE":
- env = appendIfMissing(env, "GOMODCACHE="+path)
- }
- }
-
- return deduplicateStrings(env)
-}
-
-func cacheEnvironmentName(path string) string {
- base := core.Lower(ax.Base(path))
-
- switch base {
- case "go-build", "gocache":
- return "GOCACHE"
- case "go-mod", "gomodcache":
- return "GOMODCACHE"
- default:
- return ""
- }
-}
-
-func appendIfMissing(values []string, value string) []string {
- for _, current := range values {
- if current == value {
- return values
- }
- }
- return append(values, value)
-}
-
-func applyCacheEnvironment(cfg *CacheConfig) {
- setenv := core.Setenv
- for _, env := range CacheEnvironment(cfg) {
- parts := core.SplitN(env, "=", 2)
- if len(parts) != 2 {
- continue
- }
- if set := setenv(parts[0], parts[1]); !set.OK {
- continue
- }
- }
-}
-
-func normaliseCachePath(baseDir, path string) string {
- path = core.Trim(path)
- if path == "" {
- return ""
- }
-
- if core.HasPrefix(path, "~") {
- home := core.Env("HOME")
- if home != "" {
- if path == "~" {
- return ax.Clean(home)
- }
- if core.HasPrefix(path, "~/") {
- return ax.Join(home, core.TrimPrefix(path, "~/"))
- }
- }
- }
-
- if ax.IsAbs(path) {
- return ax.Clean(path)
- }
-
- return ax.Join(baseDir, path)
-}
-
-func deduplicateStrings(values []string) []string {
- if len(values) == 0 {
- return values
- }
-
- seen := make(map[string]struct{}, len(values))
- result := make([]string, 0, len(values))
- for _, value := range values {
- if value == "" {
- continue
- }
- if _, ok := seen[value]; ok {
- continue
- }
- seen[value] = struct{}{}
- result = append(result, value)
- }
- return result
-}
-
-func firstNonEmpty(values ...string) string {
- for _, value := range values {
- if core.Trim(value) != "" {
- return value
- }
- }
- return ""
-}
-
-func (c CacheConfig) effectiveDirectory() string {
- if core.Trim(c.Dir) != "" {
- return c.Dir
- }
- return c.Directory
-}
diff --git a/pkg/build/cache_example_test.go b/pkg/build/cache_example_test.go
deleted file mode 100644
index 3802f57..0000000
--- a/pkg/build/cache_example_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleDefaultBuildCachePaths references DefaultBuildCachePaths on this package API surface.
-func ExampleDefaultBuildCachePaths() {
- _ = DefaultBuildCachePaths
- core.Println("DefaultBuildCachePaths")
- // Output: DefaultBuildCachePaths
-}
-
-// ExampleCacheConfig_MarshalYAML references CacheConfig.MarshalYAML on this package API surface.
-func ExampleCacheConfig_MarshalYAML() {
- _ = (*CacheConfig).MarshalYAML
- core.Println("CacheConfig.MarshalYAML")
- // Output: CacheConfig.MarshalYAML
-}
-
-// ExampleCacheConfig_UnmarshalYAML references CacheConfig.UnmarshalYAML on this package API surface.
-func ExampleCacheConfig_UnmarshalYAML() {
- _ = (*CacheConfig).UnmarshalYAML
- core.Println("CacheConfig.UnmarshalYAML")
- // Output: CacheConfig.UnmarshalYAML
-}
-
-// ExampleSetupCache references SetupCache on this package API surface.
-func ExampleSetupCache() {
- _ = SetupCache
- core.Println("SetupCache")
- // Output: SetupCache
-}
-
-// ExampleSetupBuildCache references SetupBuildCache on this package API surface.
-func ExampleSetupBuildCache() {
- _ = SetupBuildCache
- core.Println("SetupBuildCache")
- // Output: SetupBuildCache
-}
-
-// ExampleCacheKey references CacheKey on this package API surface.
-func ExampleCacheKey() {
- _ = CacheKey
- core.Println("CacheKey")
- // Output: CacheKey
-}
-
-// ExampleCacheKeyWithConfig references CacheKeyWithConfig on this package API surface.
-func ExampleCacheKeyWithConfig() {
- _ = CacheKeyWithConfig
- core.Println("CacheKeyWithConfig")
- // Output: CacheKeyWithConfig
-}
-
-// ExampleCacheRestoreKeys references CacheRestoreKeys on this package API surface.
-func ExampleCacheRestoreKeys() {
- _ = CacheRestoreKeys
- core.Println("CacheRestoreKeys")
- // Output: CacheRestoreKeys
-}
-
-// ExampleCacheEnvironment references CacheEnvironment on this package API surface.
-func ExampleCacheEnvironment() {
- _ = CacheEnvironment
- core.Println("CacheEnvironment")
- // Output: CacheEnvironment
-}
diff --git a/pkg/build/cache_test.go b/pkg/build/cache_test.go
deleted file mode 100644
index f0a7731..0000000
--- a/pkg/build/cache_test.go
+++ /dev/null
@@ -1,581 +0,0 @@
-package build
-
-import (
- "testing"
-
- core "dappco.re/go"
- storage "dappco.re/go/build/pkg/storage"
- yaml "gopkg.in/yaml.v3"
-)
-
-func requireCacheOK(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requireCacheError(t *testing.T, result core.Result) string {
- t.Helper()
- if result.OK {
- t.Fatal("expected error")
- }
- return result.Error()
-}
-
-func TestCache_SetupCache_Good(t *testing.T) {
- fs := storage.NewMemoryMedium()
- cfg := &CacheConfig{
- Enabled: true,
- Paths: []string{
- "cache/go-build",
- "cache/go-mod",
- },
- }
-
- requireCacheOK(t, SetupCache(fs, "/workspace/project", cfg))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("/workspace/project/.core/cache", cfg.Directory) {
- t.Fatalf("want %v, got %v", "/workspace/project/.core/cache", cfg.Directory)
- }
- if !stdlibAssertEqual([]string{"/workspace/project/cache/go-build", "/workspace/project/cache/go-mod"}, cfg.Paths) {
- t.Fatalf("want %v, got %v", []string{"/workspace/project/cache/go-build", "/workspace/project/cache/go-mod"}, cfg.Paths)
- }
- if !(fs.Exists("/workspace/project/.core/cache")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/workspace/project/cache/go-build")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/workspace/project/cache/go-mod")) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestCache_SetupBuildCache_Good(t *testing.T) {
- fs := storage.NewMemoryMedium()
- cfg := &BuildConfig{
- Build: Build{
- Cache: CacheConfig{
- Enabled: true,
- Paths: []string{
- "cache/go-build",
- },
- },
- },
- }
-
- requireCacheOK(t, SetupBuildCache(fs, "/workspace/project", cfg))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("/workspace/project/.core/cache", cfg.Build.Cache.Directory) {
- t.Fatalf("want %v, got %v", "/workspace/project/.core/cache", cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEqual([]string{"/workspace/project/cache/go-build"}, cfg.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{"/workspace/project/cache/go-build"}, cfg.Build.Cache.Paths)
- }
- if !(fs.Exists("/workspace/project/.core/cache")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/workspace/project/cache/go-build")) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestCache_SetupCache_Good_DefaultPathsWhenEnabled(t *testing.T) {
- fs := storage.NewMemoryMedium()
- cfg := &CacheConfig{
- Enabled: true,
- }
-
- requireCacheOK(t, SetupCache(fs, "/workspace/project", cfg))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("/workspace/project/.core/cache", cfg.Directory) {
- t.Fatalf("want %v, got %v", "/workspace/project/.core/cache", cfg.Directory)
- }
- if !stdlibAssertEqual([]string{"/workspace/project/cache/go-build", "/workspace/project/cache/go-mod"}, cfg.Paths) {
- t.Fatalf("want %v, got %v", []string{"/workspace/project/cache/go-build", "/workspace/project/cache/go-mod"}, cfg.Paths)
- }
- if !(fs.Exists("/workspace/project/.core/cache")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/workspace/project/cache/go-build")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/workspace/project/cache/go-mod")) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestCache_SetupBuildCache_Good_DefaultPathsWhenEnabled(t *testing.T) {
- fs := storage.NewMemoryMedium()
- cfg := &BuildConfig{
- Build: Build{
- Cache: CacheConfig{
- Enabled: true,
- },
- },
- }
-
- requireCacheOK(t, SetupBuildCache(fs, "/workspace/project", cfg))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("/workspace/project/.core/cache", cfg.Build.Cache.Directory) {
- t.Fatalf("want %v, got %v", "/workspace/project/.core/cache", cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEqual([]string{"/workspace/project/cache/go-build", "/workspace/project/cache/go-mod"}, cfg.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{"/workspace/project/cache/go-build", "/workspace/project/cache/go-mod"}, cfg.Build.Cache.Paths)
- }
- if !(fs.Exists("/workspace/project/.core/cache")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/workspace/project/cache/go-build")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/workspace/project/cache/go-mod")) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestCache_SetupCache_Good_Disabled(t *testing.T) {
- fs := storage.NewMemoryMedium()
- cfg := &CacheConfig{
- Enabled: false,
- Paths: []string{"cache/go-build"},
- }
-
- requireCacheOK(t, SetupCache(fs, "/workspace/project", cfg))
- if fs.Exists("/workspace/project/.core/cache") {
- t.Fatal("expected false")
- }
- if fs.Exists("/workspace/project/cache/go-build") {
- t.Fatal("expected false")
- }
- if !stdlibAssertEmpty(cfg.Directory) {
- t.Fatalf("expected empty, got %v", cfg.Directory)
- }
- if !stdlibAssertEqual([]string{"cache/go-build"}, cfg.Paths) {
- t.Fatalf("want %v, got %v", []string{"cache/go-build"}, cfg.Paths)
- }
-
-}
-
-func TestCache_SetupCache_Bad(t *testing.T) {
- t.Run("rejects invalid arity", func(t *testing.T) {
- err := requireCacheError(t, SetupCache())
- if !stdlibAssertContains(err, "expected 1 or 3 arguments") {
- t.Fatalf("expected %v to contain %v", err, "expected 1 or 3 arguments")
- }
-
- })
-
- t.Run("rejects a non-cache third argument", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
- err := requireCacheError(t, SetupCache(fs, "/workspace/project", CacheConfig{}))
- if !stdlibAssertContains(err, "third argument must be *CacheConfig") {
- t.Fatalf("expected %v to contain %v", err, "third argument must be *CacheConfig")
- }
-
- })
-}
-
-func TestCache_SetupCache_Ugly(t *testing.T) {
- t.Run("normalises home and absolute cache paths", func(t *testing.T) {
- t.Setenv("HOME", "/home/tester")
-
- fs := storage.NewMemoryMedium()
- cfg := &CacheConfig{
- Enabled: true,
- Paths: []string{
- "~/cache/go-build",
- "~",
- "/var/cache/go-mod",
- "/var/cache/go-mod",
- "",
- },
- }
-
- requireCacheOK(t, SetupCache(fs, "/workspace/project", cfg))
- if !stdlibAssertEqual("/workspace/project/.core/cache", cfg.Directory) {
- t.Fatalf("want %v, got %v", "/workspace/project/.core/cache", cfg.Directory)
- }
- if !stdlibAssertEqual([]string{"/home/tester/cache/go-build", "/home/tester", "/var/cache/go-mod"}, cfg.Paths) {
- t.Fatalf("want %v, got %v", []string{"/home/tester/cache/go-build", "/home/tester", "/var/cache/go-mod"}, cfg.Paths)
- }
- if !(fs.Exists("/workspace/project/.core/cache")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/home/tester/cache/go-build")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/home/tester")) {
- t.Fatal("expected true")
- }
- if !(fs.Exists("/var/cache/go-mod")) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("1-argument form wires process cache environment", func(t *testing.T) {
- t.Setenv("GOCACHE", "before")
- t.Setenv("GOMODCACHE", "before")
-
- result := SetupCache(CacheConfig{
- Enabled: true,
- Paths: []string{
- "/tmp/cache/go-build",
- "/tmp/cache/go-mod",
- },
- })
- requireCacheOK(t, result)
- if !stdlibAssertEqual("/tmp/cache/go-build", core.Getenv("GOCACHE")) {
- t.Fatalf("want %v, got %v", "/tmp/cache/go-build", core.Getenv("GOCACHE"))
- }
- if !stdlibAssertEqual("/tmp/cache/go-mod", core.Getenv("GOMODCACHE")) {
- t.Fatalf("want %v, got %v", "/tmp/cache/go-mod", core.Getenv("GOMODCACHE"))
- }
-
- })
-}
-
-func TestCache_SetupBuildCache_Good_Disabled(t *testing.T) {
- fs := storage.NewMemoryMedium()
- cfg := &BuildConfig{
- Build: Build{
- Cache: CacheConfig{
- Enabled: false,
- Paths: []string{"cache/go-build"},
- },
- },
- }
-
- requireCacheOK(t, SetupBuildCache(fs, "/workspace/project", cfg))
- if fs.Exists("/workspace/project/.core/cache") {
- t.Fatal("expected false")
- }
- if !stdlibAssertEmpty(cfg.Build.Cache.Directory) {
- t.Fatalf("expected empty, got %v", cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEqual([]string{"cache/go-build"}, cfg.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{"cache/go-build"}, cfg.Build.Cache.Paths)
- }
-
-}
-
-func TestCache_SetupBuildCache_Bad(t *testing.T) {
- t.Run("nil filesystem is a no-op", func(t *testing.T) {
- cfg := &BuildConfig{
- Build: Build{
- Cache: CacheConfig{Enabled: true},
- },
- }
-
- requireCacheOK(t, SetupBuildCache(nil, "/workspace/project", cfg))
- if !stdlibAssertEmpty(cfg.Build.Cache.Directory) {
- t.Fatalf("expected empty, got %v", cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEmpty(cfg.Build.Cache.Paths) {
- t.Fatalf("expected empty, got %v", cfg.Build.Cache.Paths)
- }
-
- })
-
- t.Run("nil config is a no-op", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- requireCacheOK(t, SetupBuildCache(fs, "/workspace/project", nil))
-
- })
-}
-
-func TestCache_CacheKey_Good(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireCacheOK(t, fs.Write("/workspace/project/go.sum", "module.example v1.0.0 h1:abc123"))
- requireCacheOK(t, fs.Write("/workspace/project/go.work.sum", "workspace.example v1.0.0 h1:def456"))
-
- first := CacheKey(fs, "/workspace/project", "linux", "amd64")
- second := CacheKey(fs, "/workspace/project", "linux", "amd64")
- third := CacheKey(fs, "/workspace/project", "darwin", "arm64")
- if !stdlibAssertEqual(first, second) {
- t.Fatalf("want %v, got %v", first, second)
- }
- if stdlibAssertEqual(first, third) {
- t.Fatalf("did not want %v", third)
- }
- if !stdlibAssertContains(first, "go-linux-amd64-") {
- t.Fatalf("expected %v to contain %v", first, "go-linux-amd64-")
- }
-
-}
-
-func TestCache_CacheKey_Good_GoWorkSumChangesKey(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireCacheOK(t, fs.Write("/workspace/project/go.sum", "module.example v1.0.0 h1:abc123"))
-
- baseline := CacheKey(fs, "/workspace/project", "linux", "amd64")
- requireCacheOK(t, fs.Write("/workspace/project/go.work.sum", "workspace.example v1.0.0 h1:def456"))
-
- updated := CacheKey(fs, "/workspace/project", "linux", "amd64")
- if stdlibAssertEqual(baseline, updated) {
- t.Fatalf("did not want %v", updated)
- }
-
-}
-
-func TestCache_CacheEnvironment_Good(t *testing.T) {
- t.Run("maps cache directory and Go cache paths to env vars", func(t *testing.T) {
- env := CacheEnvironment(&CacheConfig{
- Enabled: true,
- Paths: []string{
- "/workspace/project/cache/go-build",
- "/workspace/project/cache/go-mod",
- "/workspace/project/cache/go-build",
- },
- })
- if !stdlibAssertEqual([]string{"GOCACHE=/workspace/project/cache/go-build", "GOMODCACHE=/workspace/project/cache/go-mod"}, env) {
- t.Fatalf("want %v, got %v", []string{"GOCACHE=/workspace/project/cache/go-build", "GOMODCACHE=/workspace/project/cache/go-mod"}, env)
- }
-
- })
-
- t.Run("disabled cache returns no env vars", func(t *testing.T) {
- if !stdlibAssertNil(CacheEnvironment(&CacheConfig{Enabled: false})) {
- t.Fatalf("expected nil, got %v", CacheEnvironment(&CacheConfig{Enabled: false}))
- }
-
- })
-}
-
-func TestCache_CacheKeyWithConfig_Good(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireCacheOK(t, fs.Write("/workspace/project/go.sum", "module.example v1.0.0 h1:abc123"))
-
- base := CacheKey(fs, "/workspace/project", "linux", "amd64")
- key := CacheKeyWithConfig(fs, "/workspace/project", "linux", "amd64", &CacheConfig{
- KeyPrefix: "demo",
- })
- if !stdlibAssertEqual("demo-"+base, key) {
- t.Fatalf("want %v, got %v", "demo-"+base, key)
- }
-
-}
-
-func TestCache_CacheKeyWithConfig_Bad(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireCacheOK(t, fs.Write("/workspace/project/go.sum", "module.example v1.0.0 h1:abc123"))
-
- base := CacheKey(fs, "/workspace/project", "linux", "amd64")
-
- t.Run("nil config leaves key unchanged", func(t *testing.T) {
- if !stdlibAssertEqual(base, CacheKeyWithConfig(fs, "/workspace/project", "linux", "amd64", nil)) {
- t.Fatalf("want %v, got %v", base, CacheKeyWithConfig(fs, "/workspace/project", "linux", "amd64", nil))
- }
-
- })
-
- t.Run("blank prefix leaves key unchanged", func(t *testing.T) {
- if !stdlibAssertEqual(base, CacheKeyWithConfig(fs, "/workspace/project", "linux", "amd64", &CacheConfig{})) {
- t.Fatalf("want %v, got %v", base, CacheKeyWithConfig(fs, "/workspace/project", "linux", "amd64", &CacheConfig{}))
- }
-
- })
-}
-
-func TestCache_CacheKeyWithConfig_Ugly(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireCacheOK(t, fs.Write("/workspace/project/go.sum", "module.example v1.0.0 h1:abc123"))
-
- base := CacheKey(fs, "/workspace/project", "linux", "amd64")
- key := CacheKeyWithConfig(fs, "/workspace/project", "linux", "amd64", &CacheConfig{
- KeyPrefix: " demo ",
- })
- if !stdlibAssertEqual("demo-"+base, key) {
- t.Fatalf("want %v, got %v", "demo-"+base, key)
- }
-
-}
-
-func TestCache_CacheRestoreKeys_Good(t *testing.T) {
- keys := CacheRestoreKeys(&CacheConfig{
- KeyPrefix: "demo",
- RestoreKeys: []string{"go-", "core-"},
- })
- if !stdlibAssertEqual([]string{"demo", "go-", "core-"}, keys) {
- t.Fatalf("want %v, got %v", []string{"demo", "go-", "core-"}, keys)
- }
-
-}
-
-func TestCache_CacheRestoreKeys_Bad(t *testing.T) {
- t.Run("nil config returns nil", func(t *testing.T) {
- if !stdlibAssertNil(CacheRestoreKeys(nil)) {
- t.Fatalf("expected nil, got %v", CacheRestoreKeys(nil))
- }
-
- })
-
- t.Run("blank prefix is ignored", func(t *testing.T) {
- keys := CacheRestoreKeys(&CacheConfig{
- RestoreKeys: []string{"go-"},
- })
- if !stdlibAssertEqual([]string{"go-"}, keys) {
- t.Fatalf("want %v, got %v", []string{"go-"}, keys)
- }
-
- })
-}
-
-func TestCache_CacheRestoreKeys_Ugly(t *testing.T) {
- keys := CacheRestoreKeys(&CacheConfig{
- KeyPrefix: "demo",
- RestoreKeys: []string{"go-", "", "core-", "go-", "core-"},
- })
- if !stdlibAssertEqual([]string{"demo", "go-", "core-"}, keys) {
- t.Fatalf("want %v, got %v", []string{"demo", "go-", "core-"}, keys)
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCache_DefaultBuildCachePaths_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultBuildCachePaths(core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCache_DefaultBuildCachePaths_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultBuildCachePaths("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCache_DefaultBuildCachePaths_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultBuildCachePaths(core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCache_CacheConfig_MarshalYAML_Good(t *core.T) {
- subject := CacheConfig{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.MarshalYAML()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCache_CacheConfig_MarshalYAML_Bad(t *core.T) {
- subject := CacheConfig{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.MarshalYAML()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCache_CacheConfig_MarshalYAML_Ugly(t *core.T) {
- subject := CacheConfig{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.MarshalYAML()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCache_CacheConfig_UnmarshalYAML_Good(t *core.T) {
- subject := &CacheConfig{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.UnmarshalYAML(&yaml.Node{Kind: yaml.ScalarNode, Value: "false"})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCache_CacheConfig_UnmarshalYAML_Bad(t *core.T) {
- subject := &CacheConfig{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.UnmarshalYAML(&yaml.Node{Kind: yaml.ScalarNode, Value: "false"})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCache_CacheConfig_UnmarshalYAML_Ugly(t *core.T) {
- subject := &CacheConfig{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.UnmarshalYAML(&yaml.Node{Kind: yaml.ScalarNode, Value: "false"})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCache_SetupBuildCache_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = SetupBuildCache(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), &BuildConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCache_CacheKey_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CacheKey(storage.NewMemoryMedium(), "", "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCache_CacheKey_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CacheKey(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), "linux", "amd64")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCache_CacheEnvironment_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CacheEnvironment(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCache_CacheEnvironment_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CacheEnvironment(&CacheConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/checksum.go b/pkg/build/checksum.go
deleted file mode 100644
index 735a3d9..0000000
--- a/pkg/build/checksum.go
+++ /dev/null
@@ -1,121 +0,0 @@
-// Package build provides project type detection and cross-compilation for the Core build system.
-package build
-
-import (
- "crypto/sha256"
- "encoding/hex"
- stdio "io"
- "slices"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- io_interface "dappco.re/go/build/pkg/storage"
-)
-
-// Checksum computes SHA256 for an artifact and returns the artifact with the Checksum field filled.
-//
-// cs, err := build.Checksum(io.Local, artifact)
-func Checksum(fs io_interface.Medium, artifact Artifact) core.Result {
- if artifact.Path == "" {
- return core.Fail(core.E("build.Checksum", "artifact path is empty", nil))
- }
-
- // Open the file
- file := fs.Open(artifact.Path)
- if !file.OK {
- return core.Fail(core.E("build.Checksum", "failed to open file", core.NewError(file.Error())))
- }
- stream := file.Value.(core.FsFile)
- defer func() { _ = stream.Close() }()
-
- // Compute SHA256 hash
- hasher := sha256.New()
- if _, err := stdio.Copy(hasher, stream); err != nil {
- return core.Fail(core.E("build.Checksum", "failed to hash file", err))
- }
-
- checksum := hex.EncodeToString(hasher.Sum(nil))
-
- return core.Ok(Artifact{
- Path: artifact.Path,
- OS: artifact.OS,
- Arch: artifact.Arch,
- Checksum: checksum,
- })
-}
-
-// ChecksumAll computes checksums for all artifacts.
-// Returns a slice of artifacts with their Checksum fields filled.
-//
-// checked, err := build.ChecksumAll(io.Local, artifacts)
-func ChecksumAll(fs io_interface.Medium, artifacts []Artifact) core.Result {
- if len(artifacts) == 0 {
- return core.Ok([]Artifact(nil))
- }
-
- var checksummed []Artifact
- for _, artifact := range artifacts {
- cs := Checksum(fs, artifact)
- if !cs.OK {
- return core.Fail(core.E("build.ChecksumAll", "failed to checksum "+artifact.Path, core.NewError(cs.Error())))
- }
- checksummed = append(checksummed, cs.Value.(Artifact))
- }
-
- return core.Ok(checksummed)
-}
-
-// WriteChecksumFile writes a CHECKSUMS.txt file with the format:
-//
-// sha256hash filename1
-// sha256hash filename2
-//
-// The artifacts should have their Checksum fields filled (call ChecksumAll first).
-// Filenames are relative to the output directory (just the basename).
-//
-// err := build.WriteChecksumFile(io.Local, artifacts, "dist/CHECKSUMS.txt")
-func WriteChecksumFile(fs io_interface.Medium, artifacts []Artifact, path string) core.Result {
- if len(artifacts) == 0 {
- return core.Ok(nil)
- }
-
- // Build the content
- var lines []string
- for _, artifact := range artifacts {
- if artifact.Checksum == "" {
- return core.Fail(core.E("build.WriteChecksumFile", "artifact "+artifact.Path+" has no checksum", nil))
- }
- filename := checksumFilename(path, artifact.Path)
- lines = append(lines, core.Sprintf("%s %s", artifact.Checksum, filename))
- }
-
- // Sort lines for consistent output
- slices.Sort(lines)
-
- content := core.Concat(core.Join("\n", lines...), "\n")
-
- // Write the file using the medium (which handles directory creation in Write)
- written := fs.Write(path, content)
- if !written.OK {
- return core.Fail(core.E("build.WriteChecksumFile", "failed to write file", core.NewError(written.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func checksumFilename(checksumPath, artifactPath string) string {
- baseDir := ax.Dir(checksumPath)
- relativePath := ax.Rel(baseDir, artifactPath)
- if relativePath.OK {
- relativePathValue := ax.Clean(relativePath.Value.(string))
- if relativePathValue != "" &&
- relativePathValue != "." &&
- relativePathValue != ".." &&
- !ax.IsAbs(relativePathValue) &&
- !core.HasPrefix(relativePathValue, ".."+ax.DS()) {
- return core.Replace(relativePathValue, ax.DS(), "/")
- }
- }
-
- return core.PathBase(artifactPath)
-}
diff --git a/pkg/build/checksum_example_test.go b/pkg/build/checksum_example_test.go
deleted file mode 100644
index ab11637..0000000
--- a/pkg/build/checksum_example_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleChecksum references Checksum on this package API surface.
-func ExampleChecksum() {
- _ = Checksum
- core.Println("Checksum")
- // Output: Checksum
-}
-
-// ExampleChecksumAll references ChecksumAll on this package API surface.
-func ExampleChecksumAll() {
- _ = ChecksumAll
- core.Println("ChecksumAll")
- // Output: ChecksumAll
-}
-
-// ExampleWriteChecksumFile references WriteChecksumFile on this package API surface.
-func ExampleWriteChecksumFile() {
- _ = WriteChecksumFile
- core.Println("WriteChecksumFile")
- // Output: WriteChecksumFile
-}
diff --git a/pkg/build/checksum_test.go b/pkg/build/checksum_test.go
deleted file mode 100644
index a8b44e7..0000000
--- a/pkg/build/checksum_test.go
+++ /dev/null
@@ -1,408 +0,0 @@
-package build
-
-import (
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// setupChecksumTestFile creates a test file with known content.
-func setupChecksumTestFile(t *testing.T, content string) string {
- t.Helper()
-
- dir := t.TempDir()
- path := ax.Join(dir, "testfile")
- result := ax.WriteFile(path, []byte(content), 0644)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- return path
-}
-
-func requireChecksumArtifact(t *testing.T, result core.Result) Artifact {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(Artifact)
-}
-
-func requireChecksumArtifacts(t *testing.T, result core.Result) []Artifact {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if result.Value == nil {
- return nil
- }
- return result.Value.([]Artifact)
-}
-
-func requireChecksumOK(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requireChecksumBytes(t *testing.T, result core.Result) []byte {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]byte)
-}
-
-func TestChecksum_Checksum_Good(t *testing.T) {
- fs := storage.Local
- t.Run("computes SHA256 checksum", func(t *testing.T) {
- // Known SHA256 of "Hello, World!\n"
- path := setupChecksumTestFile(t, "Hello, World!\n")
- expectedChecksum := "c98c24b677eff44860afea6f493bbaec5bb1c4cbb209c6fc2bbb47f66ff2ad31"
-
- artifact := Artifact{
- Path: path,
- OS: "linux",
- Arch: "amd64",
- }
-
- result := requireChecksumArtifact(t, Checksum(fs, artifact))
- if !stdlibAssertEqual(expectedChecksum, result.Checksum) {
- t.Fatalf("want %v, got %v", expectedChecksum, result.Checksum)
- }
-
- })
-
- t.Run("preserves artifact fields", func(t *testing.T) {
- path := setupChecksumTestFile(t, "test content")
-
- artifact := Artifact{
- Path: path,
- OS: "darwin",
- Arch: "arm64",
- }
-
- result := requireChecksumArtifact(t, Checksum(fs, artifact))
- if !stdlibAssertEqual(path, result.Path) {
- t.Fatalf("want %v, got %v", path, result.Path)
- }
- if !stdlibAssertEqual("darwin", result.OS) {
- t.Fatalf("want %v, got %v", "darwin", result.OS)
- }
- if !stdlibAssertEqual("arm64", result.Arch) {
- t.Fatalf("want %v, got %v", "arm64", result.Arch)
- }
- if stdlibAssertEmpty(result.Checksum) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("produces 64 character hex string", func(t *testing.T) {
- path := setupChecksumTestFile(t, "any content")
-
- artifact := Artifact{Path: path, OS: "linux", Arch: "amd64"}
-
- result := requireChecksumArtifact(t, Checksum(fs, artifact))
- if len(result.Checksum) != 64 {
- t.Fatalf("want len %v, got %v", 64, len(result.Checksum))
- }
-
- })
-
- t.Run("different content produces different checksums", func(t *testing.T) {
- path1 := setupChecksumTestFile(t, "content one")
- path2 := setupChecksumTestFile(t, "content two")
-
- result1 := requireChecksumArtifact(t, Checksum(fs, Artifact{Path: path1, OS: "linux", Arch: "amd64"}))
-
- result2 := requireChecksumArtifact(t, Checksum(fs, Artifact{Path: path2, OS: "linux", Arch: "amd64"}))
- if stdlibAssertEqual(result1.Checksum, result2.Checksum) {
- t.Fatalf("did not want %v", result2.Checksum)
- }
-
- })
-
- t.Run("same content produces same checksum", func(t *testing.T) {
- content := "identical content"
- path1 := setupChecksumTestFile(t, content)
- path2 := setupChecksumTestFile(t, content)
-
- result1 := requireChecksumArtifact(t, Checksum(fs, Artifact{Path: path1, OS: "linux", Arch: "amd64"}))
-
- result2 := requireChecksumArtifact(t, Checksum(fs, Artifact{Path: path2, OS: "linux", Arch: "amd64"}))
- if !stdlibAssertEqual(result1.Checksum, result2.Checksum) {
- t.Fatalf("want %v, got %v", result1.Checksum, result2.Checksum)
- }
-
- })
-}
-
-func TestChecksum_Checksum_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("returns error for empty path", func(t *testing.T) {
- artifact := Artifact{
- Path: "",
- OS: "linux",
- Arch: "amd64",
- }
-
- result := Checksum(fs, artifact)
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "artifact path is empty") {
- t.Fatalf("expected %v to contain %v", result.Error(), "artifact path is empty")
- }
-
- })
-
- t.Run("returns error for non-existent file", func(t *testing.T) {
- artifact := Artifact{
- Path: "/nonexistent/path/file",
- OS: "linux",
- Arch: "amd64",
- }
-
- result := Checksum(fs, artifact)
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "failed to open file") {
- t.Fatalf("expected %v to contain %v", result.Error(), "failed to open file")
- }
-
- })
-}
-
-func TestChecksum_ChecksumAll_Good(t *testing.T) {
- fs := storage.Local
- t.Run("checksums multiple artifacts", func(t *testing.T) {
- paths := []string{
- setupChecksumTestFile(t, "content one"),
- setupChecksumTestFile(t, "content two"),
- setupChecksumTestFile(t, "content three"),
- }
-
- artifacts := []Artifact{
- {Path: paths[0], OS: "linux", Arch: "amd64"},
- {Path: paths[1], OS: "darwin", Arch: "arm64"},
- {Path: paths[2], OS: "windows", Arch: "amd64"},
- }
-
- results := requireChecksumArtifacts(t, ChecksumAll(fs, artifacts))
- if len(results) != 3 {
- t.Fatalf("want len %v, got %v", 3, len(results))
- }
-
- for i, result := range results {
- if !stdlibAssertEqual(artifacts[i].Path, result.Path) {
- t.Fatalf("want %v, got %v", artifacts[i].Path, result.Path)
- }
- if !stdlibAssertEqual(artifacts[i].OS, result.OS) {
- t.Fatalf("want %v, got %v", artifacts[i].OS, result.OS)
- }
- if !stdlibAssertEqual(artifacts[i].Arch, result.Arch) {
- t.Fatalf("want %v, got %v", artifacts[i].Arch, result.Arch)
- }
- if stdlibAssertEmpty(result.Checksum) {
- t.Fatal("expected non-empty")
- }
-
- }
- })
-
- t.Run("returns nil for empty slice", func(t *testing.T) {
- results := requireChecksumArtifacts(t, ChecksumAll(fs, []Artifact{}))
- if !stdlibAssertNil(results) {
- t.Fatalf("expected nil, got %v", results)
- }
-
- })
-
- t.Run("returns nil for nil slice", func(t *testing.T) {
- results := requireChecksumArtifacts(t, ChecksumAll(fs, nil))
- if !stdlibAssertNil(results) {
- t.Fatalf("expected nil, got %v", results)
- }
-
- })
-}
-
-func TestChecksum_ChecksumAll_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("returns partial results on error", func(t *testing.T) {
- path := setupChecksumTestFile(t, "valid content")
-
- artifacts := []Artifact{
- {Path: path, OS: "linux", Arch: "amd64"},
- {Path: "/nonexistent/file", OS: "linux", Arch: "arm64"}, // This will fail
- }
-
- result := ChecksumAll(fs, artifacts)
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "failed to checksum") {
- t.Fatalf("expected %v to contain failed to checksum", result.Error())
- }
-
- })
-}
-
-func TestChecksum_WriteChecksumFile_Good(t *testing.T) {
- fs := storage.Local
- t.Run("writes checksum file with correct format", func(t *testing.T) {
- dir := t.TempDir()
- checksumPath := ax.Join(dir, "CHECKSUMS.txt")
-
- artifacts := []Artifact{
- {Path: "/output/app_linux_amd64.tar.gz", Checksum: "abc123def456", OS: "linux", Arch: "amd64"},
- {Path: "/output/app_darwin_arm64.tar.gz", Checksum: "789xyz000111", OS: "darwin", Arch: "arm64"},
- }
-
- requireChecksumOK(t, WriteChecksumFile(fs, artifacts, checksumPath))
-
- content := requireChecksumBytes(t, ax.ReadFile(checksumPath))
-
- lines := core.Split(core.Trim(string(content)), "\n")
- if len(lines) != 2 {
- t.Fatalf("want len %v, got %v",
-
- // Lines should be sorted alphabetically
- 2, len(lines))
- }
- if !stdlibAssertEqual("789xyz000111 app_darwin_arm64.tar.gz", lines[0]) {
- t.Fatalf("want %v, got %v", "789xyz000111 app_darwin_arm64.tar.gz", lines[0])
- }
- if !stdlibAssertEqual("abc123def456 app_linux_amd64.tar.gz", lines[1]) {
- t.Fatalf("want %v, got %v", "abc123def456 app_linux_amd64.tar.gz", lines[1])
- }
-
- })
-
- t.Run("creates parent directories", func(t *testing.T) {
- dir := t.TempDir()
- checksumPath := ax.Join(dir, "nested", "deep", "CHECKSUMS.txt")
-
- artifacts := []Artifact{
- {Path: "/output/app.tar.gz", Checksum: "abc123", OS: "linux", Arch: "amd64"},
- }
-
- requireChecksumOK(t, WriteChecksumFile(fs, artifacts, checksumPath))
- if result := ax.Stat(checksumPath); !result.OK {
- t.Fatalf("expected file to exist: %v", checksumPath)
- }
-
- })
-
- t.Run("does nothing for empty artifacts", func(t *testing.T) {
- dir := t.TempDir()
- checksumPath := ax.Join(dir, "CHECKSUMS.txt")
-
- requireChecksumOK(t, WriteChecksumFile(fs, []Artifact{}, checksumPath))
- if ax.Exists(checksumPath) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("does nothing for nil artifacts", func(t *testing.T) {
- dir := t.TempDir()
- checksumPath := ax.Join(dir, "CHECKSUMS.txt")
-
- requireChecksumOK(t, WriteChecksumFile(fs, nil, checksumPath))
-
- })
-
- t.Run("uses only basename for filenames", func(t *testing.T) {
- dir := t.TempDir()
- checksumPath := ax.Join(dir, "CHECKSUMS.txt")
-
- artifacts := []Artifact{
- {Path: "/some/deep/nested/path/myapp_linux_amd64.tar.gz", Checksum: "checksum123", OS: "linux", Arch: "amd64"},
- }
-
- requireChecksumOK(t, WriteChecksumFile(fs, artifacts, checksumPath))
-
- content := requireChecksumBytes(t, ax.ReadFile(checksumPath))
- if !stdlibAssertContains(string(content), "myapp_linux_amd64.tar.gz") {
- t.Fatalf("expected %v to contain %v", string(content), "myapp_linux_amd64.tar.gz")
- }
- if stdlibAssertContains(string(content), "/some/deep/nested/path/") {
- t.Fatalf("expected %v not to contain %v", string(content), "/some/deep/nested/path/")
- }
-
- })
-
- t.Run("uses relative paths for nested artifacts inside the output tree", func(t *testing.T) {
- dir := t.TempDir()
- checksumPath := ax.Join(dir, "CHECKSUMS.txt")
- artifactPath := ax.Join(dir, "go", "myapp_linux_amd64.tar.gz")
- requireChecksumOK(t, ax.MkdirAll(ax.Dir(artifactPath), 0o755))
-
- artifacts := []Artifact{
- {Path: artifactPath, Checksum: "checksum123", OS: "linux", Arch: "amd64"},
- }
-
- requireChecksumOK(t, WriteChecksumFile(fs, artifacts, checksumPath))
-
- content := requireChecksumBytes(t, ax.ReadFile(checksumPath))
- if !stdlibAssertContains(string(content), "go/myapp_linux_amd64.tar.gz") {
- t.Fatalf("expected %v to contain %v", string(content), "go/myapp_linux_amd64.tar.gz")
- }
-
- })
-}
-
-func TestChecksum_WriteChecksumFile_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("returns error for artifact without checksum", func(t *testing.T) {
- dir := t.TempDir()
- checksumPath := ax.Join(dir, "CHECKSUMS.txt")
-
- artifacts := []Artifact{
- {Path: "/output/app.tar.gz", Checksum: "", OS: "linux", Arch: "amd64"}, // No checksum
- }
-
- result := WriteChecksumFile(fs, artifacts, checksumPath)
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "has no checksum") {
- t.Fatalf("expected %v to contain %v", result.Error(), "has no checksum")
- }
-
- })
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestChecksum_Checksum_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Checksum(storage.NewMemoryMedium(), Artifact{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestChecksum_ChecksumAll_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ChecksumAll(storage.NewMemoryMedium(), nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestChecksum_WriteChecksumFile_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteChecksumFile(storage.NewMemoryMedium(), nil, core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/ci.go b/pkg/build/ci.go
deleted file mode 100644
index de23ec5..0000000
--- a/pkg/build/ci.go
+++ /dev/null
@@ -1,375 +0,0 @@
-// Package build provides project type detection and cross-compilation for the Core build system.
-// This file handles CI environment detection and GitHub Actions output formatting.
-package build
-
-import (
- "context"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- io_interface "dappco.re/go/build/pkg/storage"
-)
-
-// CIContext holds environment information detected from a GitHub Actions run.
-//
-// ci := build.DetectCI()
-// if ci != nil {
-// fmt.Println(ci.ShortSHA) // "abc1234"
-// }
-type CIContext struct {
- // Ref is the full git ref (GITHUB_REF).
- // ci.Ref // "refs/tags/v1.2.3"
- Ref string
- // SHA is the full commit hash (GITHUB_SHA).
- // ci.SHA // "abc1234def5678..."
- SHA string
- // ShortSHA is the first 7 characters of SHA.
- // ci.ShortSHA // "abc1234"
- ShortSHA string
- // Tag is the tag name when the ref is a tag ref.
- // ci.Tag // "v1.2.3"
- Tag string
- // IsTag is true when the ref is a tag ref (refs/tags/...).
- // ci.IsTag // true
- IsTag bool
- // Branch is the branch name when the ref is a branch ref.
- // ci.Branch // "main"
- Branch string
- // Repo is the owner/repo string (GITHUB_REPOSITORY).
- // ci.Repo // "dappcore/core"
- Repo string
- // Owner is the repository owner derived from Repo.
- // ci.Owner // "dappcore"
- Owner string
-}
-
-const artifactMetaOSField = "o" + "s"
-
-// FormatGitHubAnnotation formats a build message as a GitHub Actions annotation.
-//
-// s := build.FormatGitHubAnnotation("error", "main.go", 42, "undefined: foo")
-// // "::error file=main.go,line=42::undefined: foo"
-//
-// s := build.FormatGitHubAnnotation("warning", "pkg/build/ci.go", 10, "unused import")
-// // "::warning file=pkg/build/ci.go,line=10::unused import"
-func FormatGitHubAnnotation(level, file string, line int, message string) string {
- return core.Sprintf(
- "::%s file=%s,line=%d::%s",
- escapeGitHubAnnotationValue(level),
- escapeGitHubAnnotationValue(file),
- line,
- escapeGitHubAnnotationValue(message),
- )
-}
-
-func escapeGitHubAnnotationValue(value string) string {
- value = core.Replace(value, "%", "%25")
- value = core.Replace(value, "\r", "%0D")
- value = core.Replace(value, "\n", "%0A")
- return value
-}
-
-// DetectCI reads GitHub Actions environment variables and returns a populated CIContext.
-// Returns nil if GITHUB_ACTIONS is not set or GITHUB_SHA is empty, which indicates
-// the process is not running inside GitHub Actions.
-//
-// ci := build.DetectCI()
-// if ci == nil {
-// // running locally, skip CI-specific output
-// }
-// if ci != nil && ci.IsTag {
-// // upload release assets
-// }
-func DetectCI() *CIContext {
- return detectGitHubContext(true)
-}
-
-// DetectGitHubMetadata returns GitHub CI metadata when the standard environment
-// variables are present, even if GITHUB_ACTIONS is unset.
-//
-// This is useful for metadata emission paths that only need the GitHub ref/SHA
-// shape and should not be coupled to a specific runner environment.
-func DetectGitHubMetadata() *CIContext {
- return detectGitHubContext(false)
-}
-
-func detectLocalGitMetadata(dir string) *CIContext {
- dir = core.Trim(dir)
- if dir == "" {
- return nil
- }
-
- sha := runGitMetadataCommand(dir, "rev-parse", "HEAD")
- if !sha.OK || sha.Value.(string) == "" {
- return nil
- }
-
- ctx := &CIContext{SHA: sha.Value.(string)}
-
- if tag := runGitMetadataCommand(dir, "describe", "--tags", "--exact-match", "HEAD"); tag.OK && tag.Value.(string) != "" {
- ctx.Ref = "refs/tags/" + tag.Value.(string)
- } else if branch := runGitMetadataCommand(dir, "symbolic-ref", "--quiet", "--short", "HEAD"); branch.OK && branch.Value.(string) != "" {
- ctx.Ref = "refs/heads/" + branch.Value.(string)
- }
-
- if remoteURL := runGitMetadataCommand(dir, "remote", "get-url", "origin"); remoteURL.OK {
- ctx.Repo, ctx.Owner = parseGitRemote(remoteURL.Value.(string))
- }
-
- populateGitHubContext(ctx)
- return ctx
-}
-
-func detectGitHubContext(requireActions bool) *CIContext {
- if requireActions && core.Env("GITHUB_ACTIONS") == "" {
- return nil
- }
-
- sha := core.Env("GITHUB_SHA")
- if sha == "" {
- return nil
- }
-
- ref := core.Env("GITHUB_REF")
- repo := core.Env("GITHUB_REPOSITORY")
-
- ctx := &CIContext{
- Ref: ref,
- SHA: sha,
- Repo: repo,
- }
-
- populateGitHubContext(ctx)
- return ctx
-}
-
-func populateGitHubContext(ctx *CIContext) {
- if ctx == nil {
- return
- }
-
- // ShortSHA is first 7 chars of SHA.
- runes := []rune(ctx.SHA)
- if len(runes) >= 7 {
- ctx.ShortSHA = string(runes[:7])
- } else {
- ctx.ShortSHA = ctx.SHA
- }
-
- // Derive owner from "owner/repo" format.
- if ctx.Repo != "" {
- parts := core.SplitN(ctx.Repo, "/", 2)
- if len(parts) == 2 {
- ctx.Owner = parts[0]
- }
- }
-
- // Classify ref as tag or branch.
- const tagPrefix = "refs/tags/"
- const branchPrefix = "refs/heads/"
-
- if core.HasPrefix(ctx.Ref, tagPrefix) {
- ctx.IsTag = true
- ctx.Tag = core.TrimPrefix(ctx.Ref, tagPrefix)
- } else if core.HasPrefix(ctx.Ref, branchPrefix) {
- ctx.Branch = core.TrimPrefix(ctx.Ref, branchPrefix)
- }
-}
-
-func runGitMetadataCommand(dir string, args ...string) core.Result {
- output := ax.RunDir(context.Background(), dir, "git", args...)
- if !output.OK {
- return output
- }
- return core.Ok(core.Trim(output.Value.(string)))
-}
-
-func parseGitRemote(raw string) (string, string) {
- raw = core.Trim(raw)
- if raw == "" {
- return "", ""
- }
-
- path := remoteRepositoryPath(raw)
- if path == "" {
- return "", ""
- }
-
- path = core.Replace(path, "\\", "/")
- parts := core.Split(path, "/")
- if len(parts) < 2 {
- return "", ""
- }
-
- owner := parts[len(parts)-2]
- repo := core.TrimSuffix(parts[len(parts)-1], ".git")
- if owner == "" || repo == "" {
- return "", ""
- }
-
- value := owner + "/" + repo
- return value, owner
-}
-
-func remoteRepositoryPath(raw string) string {
- if splitURL := core.SplitN(raw, "://", 2); len(splitURL) == 2 && splitURL[0] != "" {
- raw = splitURL[1]
- pathParts := core.SplitN(raw, "/", 2)
- if len(pathParts) != 2 {
- return ""
- }
- return trimSlashes(core.SplitN(pathParts[1], "?", 2)[0])
- }
-
- if splitSCM := core.SplitN(raw, ":", 2); len(splitSCM) == 2 && splitSCM[0] != "" && core.Contains(splitSCM[0], "@") {
- return trimSlashes(splitSCM[1])
- }
-
- return trimSlashes(raw)
-}
-
-// ArtifactName generates a canonical artifact filename from the build name, CI context, and target.
-// Format: {name}_{OS}_{ARCH}_{TAG|SHORT_SHA}
-// When ci is nil or has no tag or SHA, only the name and target are used.
-//
-// name := build.ArtifactName("core", ci, build.Target{OS: "linux", Arch: "amd64"})
-// // "core_linux_amd64_v1.2.3" (when ci.IsTag)
-// // "core_linux_amd64_abc1234" (when ci != nil, not a tag)
-// // "core_linux_amd64" (when ci is nil)
-func ArtifactName(buildName string, ci *CIContext, target Target) string {
- base := core.Join("_", buildName, target.OS, target.Arch)
-
- if ci == nil {
- return base
- }
-
- var version string
- if ci.IsTag && ci.Tag != "" {
- version = ci.Tag
- } else if ci.ShortSHA != "" {
- version = ci.ShortSHA
- }
-
- if version == "" {
- return base
- }
-
- return core.Concat(base, "_", version)
-}
-
-// WriteArtifactMeta writes an artifact_meta.json file to path.
-// The file contains the build name, target OS/arch, and CI metadata if available.
-//
-// err := build.WriteArtifactMeta(io.Local, "dist/artifact_meta.json", "core", build.Target{OS: "linux", Arch: "amd64"}, ci)
-// // writes metadata fields for name, platform, arch, tag, and CI status.
-func WriteArtifactMeta(fs io_interface.Medium, path string, buildName string, target Target, ci *CIContext) core.Result {
- meta := map[string]any{
- "name": buildName,
- artifactMetaOSField: target.OS,
- "arch": target.Arch,
- "is_tag": false,
- }
-
- if ci != nil {
- addArtifactMetaString(meta, "ref", ci.Ref)
- addArtifactMetaString(meta, "sha", ci.SHA)
- addArtifactMetaString(meta, "tag", ci.Tag)
- addArtifactMetaString(meta, "branch", ci.Branch)
- addArtifactMetaString(meta, "repo", ci.Repo)
- meta["is_tag"] = ci.IsTag
- }
-
- encodedData := core.JSONMarshalIndent(meta, "", " ")
- if !encodedData.OK {
- return core.Fail(core.E("build.WriteArtifactMeta", "failed to marshal artifact meta", core.NewError(encodedData.Error())))
- }
-
- written := fs.Write(path, string(encodedData.Value.([]byte)))
- if !written.OK {
- return core.Fail(core.E("build.WriteArtifactMeta", "failed to write artifact meta", core.NewError(written.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func addArtifactMetaString(meta map[string]any, key, value string) {
- if value != "" {
- meta[key] = value
- }
-}
-
-func trimSlashes(value string) string {
- for core.HasPrefix(value, "/") {
- value = core.TrimPrefix(value, "/")
- }
- for core.HasSuffix(value, "/") {
- value = core.TrimSuffix(value, "/")
- }
- return value
-}
-
-// CIArtifactPath returns the CI-stamped artifact path for a build output.
-// The filename keeps the original packaging suffix, such as `.tar.gz`, `.zip`,
-// `.exe`, or `.app`.
-//
-// path := build.CIArtifactPath("core", ci, build.Artifact{
-// Path: "/tmp/dist/linux_amd64/core.tar.gz",
-// OS: "linux",
-// Arch: "amd64",
-// })
-func CIArtifactPath(buildName string, ci *CIContext, artifact Artifact) string {
- if ci == nil || artifact.Path == "" || artifact.OS == "" || artifact.Arch == "" {
- return artifact.Path
- }
-
- return replaceArtifactBaseName(artifact.Path, ArtifactName(buildName, ci, Target{
- OS: artifact.OS,
- Arch: artifact.Arch,
- }))
-}
-
-func replaceArtifactBaseName(artifactPath, replacement string) string {
- if artifactPath == "" || replacement == "" {
- return artifactPath
- }
-
- baseName := ax.Base(artifactPath)
- suffix := artifactPathSuffix(baseName)
- if suffix == "" {
- return ax.Join(ax.Dir(artifactPath), replacement)
- }
-
- return ax.Join(ax.Dir(artifactPath), replacement+suffix)
-}
-
-func artifactPathSuffix(fileName string) string {
- switch {
- case core.HasSuffix(fileName, ".tar.gz"):
- return ".tar.gz"
- case core.HasSuffix(fileName, ".tar.xz"):
- return ".tar.xz"
- case core.HasSuffix(fileName, ".tar.zst"):
- return ".tar.zst"
- case core.HasSuffix(fileName, ".tar.bz2"):
- return ".tar.bz2"
- case core.HasSuffix(fileName, ".tgz"):
- return ".tgz"
- case core.HasSuffix(fileName, ".txz"):
- return ".txz"
- case core.HasSuffix(fileName, ".zip"):
- return ".zip"
- case core.HasSuffix(fileName, ".exe"):
- return ".exe"
- case core.HasSuffix(fileName, ".dmg"):
- return ".dmg"
- case core.HasSuffix(fileName, ".app"):
- return ".app"
- default:
- parts := core.Split(fileName, ".")
- if len(parts) <= 1 || (len(parts) == 2 && parts[0] == "") {
- return ""
- }
-
- return "." + parts[len(parts)-1]
- }
-}
diff --git a/pkg/build/ci_example_test.go b/pkg/build/ci_example_test.go
deleted file mode 100644
index 7d83b50..0000000
--- a/pkg/build/ci_example_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleFormatGitHubAnnotation references FormatGitHubAnnotation on this package API surface.
-func ExampleFormatGitHubAnnotation() {
- _ = FormatGitHubAnnotation
- core.Println("FormatGitHubAnnotation")
- // Output: FormatGitHubAnnotation
-}
-
-// ExampleDetectCI references DetectCI on this package API surface.
-func ExampleDetectCI() {
- _ = DetectCI
- core.Println("DetectCI")
- // Output: DetectCI
-}
-
-// ExampleDetectGitHubMetadata references DetectGitHubMetadata on this package API surface.
-func ExampleDetectGitHubMetadata() {
- _ = DetectGitHubMetadata
- core.Println("DetectGitHubMetadata")
- // Output: DetectGitHubMetadata
-}
-
-// ExampleArtifactName references ArtifactName on this package API surface.
-func ExampleArtifactName() {
- _ = ArtifactName
- core.Println("ArtifactName")
- // Output: ArtifactName
-}
-
-// ExampleWriteArtifactMeta references WriteArtifactMeta on this package API surface.
-func ExampleWriteArtifactMeta() {
- _ = WriteArtifactMeta
- core.Println("WriteArtifactMeta")
- // Output: WriteArtifactMeta
-}
-
-// ExampleCIArtifactPath references CIArtifactPath on this package API surface.
-func ExampleCIArtifactPath() {
- _ = CIArtifactPath
- core.Println("CIArtifactPath")
- // Output: CIArtifactPath
-}
diff --git a/pkg/build/ci_test.go b/pkg/build/ci_test.go
deleted file mode 100644
index 100a1dd..0000000
--- a/pkg/build/ci_test.go
+++ /dev/null
@@ -1,720 +0,0 @@
-package build
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// setenvCI sets the GitHub Actions environment variables for a test and cleans up afterwards.
-func setenvCI(t *testing.T, sha, ref, repo string) {
- t.Helper()
- t.Setenv("GITHUB_ACTIONS", "true")
- t.Setenv("GITHUB_SHA", sha)
- t.Setenv("GITHUB_REF", ref)
- t.Setenv("GITHUB_REPOSITORY", repo)
-}
-
-func initGitMetadataRepo(t *testing.T) (string, string) {
- t.Helper()
-
- dir := t.TempDir()
- ctx := context.Background()
- requireCIOK(t, ax.ExecDir(ctx, dir, "git", "init", "-b", "main"))
- requireCIOK(t, ax.ExecDir(ctx, dir, "git", "config", "user.email", "codex@example.com"))
- requireCIOK(t, ax.ExecDir(ctx, dir, "git", "config", "user.name", "Codex"))
- requireCIOK(t, ax.WriteFile(ax.Join(dir, "README.md"), []byte("# demo\n"), 0o644))
- requireCIOK(t, ax.ExecDir(ctx, dir, "git", "add", "README.md"))
- requireCIOK(t, ax.ExecDir(ctx, dir, "git", "commit", "-m", "init"))
- requireCIOK(t, ax.ExecDir(ctx, dir, "git", "remote", "add", "origin", "git@github.com:dappcore/core.git"))
-
- sha := requireCIString(t, ax.RunDir(ctx, dir, "git", "rev-parse", "HEAD"))
-
- return dir, sha
-}
-
-func requireCIOK(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requireCIString(t *testing.T, result core.Result) string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func requireCIBytes(t *testing.T, result core.Result) []byte {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]byte)
-}
-
-func TestCi_FormatGitHubAnnotation_Good(t *testing.T) {
- t.Run("formats error annotation correctly", func(t *testing.T) {
- s := FormatGitHubAnnotation("error", "main.go", 42, "undefined: foo")
- if !stdlibAssertEqual("::error file=main.go,line=42::undefined: foo", s) {
- t.Fatalf("want %v, got %v", "::error file=main.go,line=42::undefined: foo", s)
- }
-
- })
-
- t.Run("formats warning annotation correctly", func(t *testing.T) {
- s := FormatGitHubAnnotation("warning", "pkg/build/ci.go", 10, "unused import")
- if !stdlibAssertEqual("::warning file=pkg/build/ci.go,line=10::unused import", s) {
- t.Fatalf("want %v, got %v", "::warning file=pkg/build/ci.go,line=10::unused import", s)
- }
-
- })
-
- t.Run("formats notice annotation correctly", func(t *testing.T) {
- s := FormatGitHubAnnotation("notice", "cmd/main.go", 1, "build started")
- if !stdlibAssertEqual("::notice file=cmd/main.go,line=1::build started", s) {
- t.Fatalf("want %v, got %v", "::notice file=cmd/main.go,line=1::build started", s)
- }
-
- })
-
- t.Run("uses correct line numbers", func(t *testing.T) {
- s := FormatGitHubAnnotation("error", "file.go", 99, "msg")
- if !stdlibAssertContains(s, "line=99") {
- t.Fatalf("expected %v to contain %v", s, "line=99")
- }
-
- })
-}
-
-func TestCi_FormatGitHubAnnotation_Bad(t *testing.T) {
- t.Run("empty file produces empty file field", func(t *testing.T) {
- s := FormatGitHubAnnotation("error", "", 1, "message")
- if !stdlibAssertEqual("::error file=,line=1::message", s) {
- t.Fatalf("want %v, got %v", "::error file=,line=1::message", s)
- }
-
- })
-
- t.Run("empty level still produces annotation format", func(t *testing.T) {
- s := FormatGitHubAnnotation("", "main.go", 1, "message")
- if !stdlibAssertEqual(":: file=main.go,line=1::message", s) {
- t.Fatalf("want %v, got %v", ":: file=main.go,line=1::message", s)
- }
-
- })
-
- t.Run("empty message produces empty message section", func(t *testing.T) {
- s := FormatGitHubAnnotation("error", "main.go", 1, "")
- if !stdlibAssertEqual("::error file=main.go,line=1::", s) {
- t.Fatalf("want %v, got %v", "::error file=main.go,line=1::", s)
- }
-
- })
-
- t.Run("line zero is valid", func(t *testing.T) {
- s := FormatGitHubAnnotation("error", "main.go", 0, "msg")
- if !stdlibAssertContains(s, "line=0") {
- t.Fatalf("expected %v to contain %v", s, "line=0")
- }
-
- })
-}
-
-func TestCi_FormatGitHubAnnotation_Ugly(t *testing.T) {
- t.Run("message with newline is escaped", func(t *testing.T) {
- s := FormatGitHubAnnotation("error", "main.go", 1, "line one\nline two")
- if !stdlibAssertContains(s, "line one%0Aline two") {
- t.Fatalf("expected %v to contain %v", s, "line one%0Aline two")
- }
-
- })
-
- t.Run("message with colons does not break format", func(t *testing.T) {
- s := FormatGitHubAnnotation("error", "main.go", 1, "error: something::bad")
- if !stdlibAssertContains(
- // The leading ::level file=... part should still be present
- s, "::error file=main.go,line=1::") {
- t.Fatalf("expected %v to contain %v", s, "::error file=main.go,line=1::")
- }
- if !stdlibAssertContains(s, "error: something::bad") {
- t.Fatalf("expected %v to contain %v", s, "error: something::bad")
- }
-
- })
-
- t.Run("file path with spaces is included as-is", func(t *testing.T) {
- s := FormatGitHubAnnotation("warning", "my file.go", 5, "msg")
- if !stdlibAssertContains(s, "file=my file.go") {
- t.Fatalf("expected %v to contain %v", s, "file=my file.go")
- }
-
- })
-
- t.Run("unicode message is preserved", func(t *testing.T) {
- s := FormatGitHubAnnotation("error", "main.go", 1, "résumé: 日本語")
- if !stdlibAssertContains(s, "résumé: 日本語") {
- t.Fatalf("expected %v to contain %v", s, "résumé: 日本語")
- }
-
- })
-
- t.Run("percent characters are escaped for GitHub annotations", func(t *testing.T) {
- s := FormatGitHubAnnotation("error", "main.go", 1, "100% done")
- if !stdlibAssertContains(s, "100%25 done") {
- t.Fatalf("expected %v to contain %v", s, "100%25 done")
- }
-
- })
-}
-
-func TestCi_DetectCI_Good(t *testing.T) {
- t.Run("detects tag ref", func(t *testing.T) {
- setenvCI(t, "abc1234def5678901234567890123456789012345", "refs/tags/v1.2.3", "dappcore/core")
-
- ci := DetectCI()
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("refs/tags/v1.2.3", ci.Ref) {
- t.Fatalf("want %v, got %v", "refs/tags/v1.2.3", ci.Ref)
- }
- if !stdlibAssertEqual("abc1234def5678901234567890123456789012345", ci.SHA) {
- t.Fatalf("want %v, got %v", "abc1234def5678901234567890123456789012345", ci.SHA)
- }
- if !stdlibAssertEqual("abc1234", ci.ShortSHA) {
- t.Fatalf("want %v, got %v", "abc1234", ci.ShortSHA)
- }
- if !stdlibAssertEqual("v1.2.3", ci.Tag) {
- t.Fatalf("want %v, got %v", "v1.2.3", ci.Tag)
- }
- if !(ci.IsTag) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("", ci.Branch) {
- t.Fatalf("want %v, got %v", "", ci.Branch)
- }
- if !stdlibAssertEqual("dappcore/core", ci.Repo) {
- t.Fatalf("want %v, got %v", "dappcore/core", ci.Repo)
- }
- if !stdlibAssertEqual("dappcore", ci.Owner) {
- t.Fatalf("want %v, got %v", "dappcore", ci.Owner)
- }
-
- })
-
- t.Run("detects branch ref", func(t *testing.T) {
- setenvCI(t, "deadbeef1234567890123456789012345678abcd", "refs/heads/main", "org/repo")
-
- ci := DetectCI()
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("main", ci.Branch) {
- t.Fatalf("want %v, got %v", "main", ci.Branch)
- }
- if ci.IsTag {
- t.Fatal("expected false")
- }
- if !stdlibAssertEqual("", ci.Tag) {
- t.Fatalf("want %v, got %v", "", ci.Tag)
- }
- if !stdlibAssertEqual("deadbee", ci.ShortSHA) {
- t.Fatalf("want %v, got %v", "deadbee", ci.ShortSHA)
- }
-
- })
-
- t.Run("owner is derived from repo", func(t *testing.T) {
- setenvCI(t, "aaaaaaaaaaaaaaaa", "refs/heads/dev", "myorg/myrepo")
-
- ci := DetectCI()
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("myorg", ci.Owner) {
- t.Fatalf("want %v, got %v", "myorg", ci.Owner)
- }
- if !stdlibAssertEqual("myorg/myrepo", ci.Repo) {
- t.Fatalf("want %v, got %v", "myorg/myrepo", ci.Repo)
- }
-
- })
-}
-
-func TestCi_DetectCI_Bad(t *testing.T) {
- t.Run("returns nil when GITHUB_ACTIONS is not set", func(t *testing.T) {
- t.Setenv("GITHUB_ACTIONS", "")
- t.Setenv("GITHUB_SHA", "abc1234def5678901234567890123456789012345")
- t.Setenv("GITHUB_REF", "refs/heads/main")
- t.Setenv("GITHUB_REPOSITORY", "org/repo")
-
- ci := DetectCI()
- if !stdlibAssertNil(ci) {
- t.Fatalf("expected nil, got %v", ci)
- }
-
- })
-
- t.Run("returns nil when GITHUB_SHA is not set", func(t *testing.T) {
- t.Setenv("GITHUB_ACTIONS", "true")
- t.Setenv("GITHUB_SHA", "")
- t.Setenv("GITHUB_REF", "")
- t.Setenv("GITHUB_REPOSITORY", "")
-
- ci := DetectCI()
- if !stdlibAssertNil(ci) {
- t.Fatalf("expected nil, got %v", ci)
- }
-
- })
-}
-
-func TestCi_DetectGitHubMetadata_Good(t *testing.T) {
- t.Run("detects GitHub metadata without GITHUB_ACTIONS", func(t *testing.T) {
- t.Setenv("GITHUB_ACTIONS", "")
- t.Setenv("GITHUB_SHA", "abc1234def5678901234567890123456789012345")
- t.Setenv("GITHUB_REF", "refs/heads/main")
- t.Setenv("GITHUB_REPOSITORY", "org/repo")
-
- ci := DetectGitHubMetadata()
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("abc1234", ci.ShortSHA) {
- t.Fatalf("want %v, got %v", "abc1234", ci.ShortSHA)
- }
- if !stdlibAssertEqual("main", ci.Branch) {
- t.Fatalf("want %v, got %v", "main", ci.Branch)
- }
- if !stdlibAssertEqual("org/repo", ci.Repo) {
- t.Fatalf("want %v, got %v", "org/repo", ci.Repo)
- }
- if !stdlibAssertEqual("org", ci.Owner) {
- t.Fatalf("want %v, got %v", "org", ci.Owner)
- }
-
- })
-}
-
-func TestCi_detectLocalGitMetadata_Good(t *testing.T) {
- t.Run("detects branch metadata from local git repository", func(t *testing.T) {
- dir, sha := initGitMetadataRepo(t)
-
- ci := detectLocalGitMetadata(dir)
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(sha, ci.SHA) {
- t.Fatalf("want %v, got %v", sha, ci.SHA)
- }
- if !stdlibAssertEqual(sha[:7], ci.ShortSHA) {
- t.Fatalf("want %v, got %v", sha[:7], ci.ShortSHA)
- }
- if !stdlibAssertEqual("refs/heads/main", ci.Ref) {
- t.Fatalf("want %v, got %v", "refs/heads/main", ci.Ref)
- }
- if !stdlibAssertEqual("main", ci.Branch) {
- t.Fatalf("want %v, got %v", "main", ci.Branch)
- }
- if ci.IsTag {
- t.Fatal("expected false")
- }
- if !stdlibAssertEqual("", ci.Tag) {
- t.Fatalf("want %v, got %v", "", ci.Tag)
- }
- if !stdlibAssertEqual("dappcore/core", ci.Repo) {
- t.Fatalf("want %v, got %v", "dappcore/core", ci.Repo)
- }
- if !stdlibAssertEqual("dappcore", ci.Owner) {
- t.Fatalf("want %v, got %v", "dappcore", ci.Owner)
- }
-
- })
-
- t.Run("prefers exact tag metadata when HEAD is tagged", func(t *testing.T) {
- dir, sha := initGitMetadataRepo(t)
- requireCIOK(t, ax.ExecDir(context.Background(), dir, "git", "tag", "v1.2.3"))
-
- ci := detectLocalGitMetadata(dir)
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(sha, ci.SHA) {
- t.Fatalf("want %v, got %v", sha, ci.SHA)
- }
- if !stdlibAssertEqual("refs/tags/v1.2.3", ci.Ref) {
- t.Fatalf("want %v, got %v", "refs/tags/v1.2.3", ci.Ref)
- }
- if !(ci.IsTag) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("v1.2.3", ci.Tag) {
- t.Fatalf("want %v, got %v", "v1.2.3", ci.Tag)
- }
- if !stdlibAssertEqual("", ci.Branch) {
- t.Fatalf("want %v, got %v", "", ci.Branch)
- }
-
- })
-}
-
-func TestCi_detectLocalGitMetadata_Bad(t *testing.T) {
- t.Run("returns nil outside a git repository", func(t *testing.T) {
- if !stdlibAssertNil(detectLocalGitMetadata(t.TempDir())) {
- t.Fatalf("expected nil, got %v", detectLocalGitMetadata(t.TempDir()))
- }
-
- })
-}
-
-func TestCi_DetectCI_Ugly(t *testing.T) {
- t.Run("SHA shorter than 7 chars still works", func(t *testing.T) {
- setenvCI(t, "abc", "refs/heads/main", "org/repo")
-
- ci := DetectCI()
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("abc", ci.ShortSHA) {
- t.Fatalf("want %v, got %v", "abc", ci.ShortSHA)
- }
-
- })
-
- t.Run("ref with unknown prefix leaves tag and branch empty", func(t *testing.T) {
- setenvCI(t, "abc1234def5678", "refs/pull/42/merge", "org/repo")
-
- ci := DetectCI()
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("", ci.Tag) {
- t.Fatalf("want %v, got %v", "", ci.Tag)
- }
- if !stdlibAssertEqual("", ci.Branch) {
- t.Fatalf("want %v, got %v", "", ci.Branch)
- }
- if ci.IsTag {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("repo without slash leaves owner empty", func(t *testing.T) {
- setenvCI(t, "abc1234def5678", "refs/heads/main", "noslashrepo")
-
- ci := DetectCI()
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("", ci.Owner) {
- t.Fatalf("want %v, got %v", "", ci.Owner)
- }
- if !stdlibAssertEqual("noslashrepo", ci.Repo) {
- t.Fatalf("want %v, got %v", "noslashrepo", ci.Repo)
- }
-
- })
-
- t.Run("empty repo is tolerated", func(t *testing.T) {
- setenvCI(t, "abc1234def5678", "refs/heads/main", "")
-
- ci := DetectCI()
- if stdlibAssertNil(ci) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("", ci.Owner) {
- t.Fatalf("want %v, got %v", "", ci.Owner)
- }
- if !stdlibAssertEqual("", ci.Repo) {
- t.Fatalf("want %v, got %v", "", ci.Repo)
- }
-
- })
-}
-
-func TestCi_ArtifactName_Good(t *testing.T) {
- t.Run("uses tag when IsTag is true", func(t *testing.T) {
- ci := &CIContext{
- IsTag: true,
- Tag: "v1.2.3",
- ShortSHA: "abc1234",
- }
- name := ArtifactName("core", ci, Target{OS: "linux", Arch: "amd64"})
- if !stdlibAssertEqual("core_linux_amd64_v1.2.3", name) {
- t.Fatalf("want %v, got %v", "core_linux_amd64_v1.2.3", name)
- }
-
- })
-
- t.Run("uses ShortSHA when not a tag", func(t *testing.T) {
- ci := &CIContext{
- IsTag: false,
- ShortSHA: "abc1234",
- }
- name := ArtifactName("myapp", ci, Target{OS: "darwin", Arch: "arm64"})
- if !stdlibAssertEqual("myapp_darwin_arm64_abc1234", name) {
- t.Fatalf("want %v, got %v", "myapp_darwin_arm64_abc1234", name)
- }
-
- })
-
- t.Run("produces correct format for windows", func(t *testing.T) {
- ci := &CIContext{IsTag: true, Tag: "v2.0.0", ShortSHA: "ff00ff0"}
- name := ArtifactName("core", ci, Target{OS: "windows", Arch: "amd64"})
- if !stdlibAssertEqual("core_windows_amd64_v2.0.0", name) {
- t.Fatalf("want %v, got %v", "core_windows_amd64_v2.0.0", name)
- }
-
- })
-}
-
-func TestCi_ArtifactName_Bad(t *testing.T) {
- t.Run("nil ci returns name_os_arch only", func(t *testing.T) {
- name := ArtifactName("core", nil, Target{OS: "linux", Arch: "amd64"})
- if !stdlibAssertEqual("core_linux_amd64", name) {
- t.Fatalf("want %v, got %v", "core_linux_amd64", name)
- }
-
- })
-
- t.Run("ci with no tag and no SHA returns name_os_arch only", func(t *testing.T) {
- ci := &CIContext{IsTag: false, ShortSHA: "", Tag: ""}
- name := ArtifactName("core", ci, Target{OS: "linux", Arch: "amd64"})
- if !stdlibAssertEqual("core_linux_amd64", name) {
- t.Fatalf("want %v, got %v", "core_linux_amd64", name)
- }
-
- })
-}
-
-func TestCi_ArtifactName_Ugly(t *testing.T) {
- t.Run("empty build name produces leading underscore segments", func(t *testing.T) {
- ci := &CIContext{IsTag: true, Tag: "v1.0.0", ShortSHA: "abc1234"}
- name := ArtifactName("", ci, Target{OS: "linux", Arch: "amd64"})
- if !stdlibAssertContains(
- // Empty name results in "_linux_amd64_v1.0.0"
- name, "linux_amd64_v1.0.0") {
- t.Fatalf("expected %v to contain %v", name, "linux_amd64_v1.0.0")
- }
-
- })
-
- t.Run("IsTag true but empty tag falls back to ShortSHA", func(t *testing.T) {
- ci := &CIContext{IsTag: true, Tag: "", ShortSHA: "abc1234"}
- name := ArtifactName("core", ci, Target{OS: "linux", Arch: "amd64"})
- if !stdlibAssertEqual("core_linux_amd64_abc1234", name) {
- t.Fatalf("want %v, got %v", "core_linux_amd64_abc1234", name)
- }
-
- })
-
- t.Run("special chars in build name are preserved", func(t *testing.T) {
- ci := &CIContext{IsTag: true, Tag: "v1.0.0"}
- name := ArtifactName("core-build", ci, Target{OS: "linux", Arch: "amd64"})
- if !stdlibAssertEqual("core-build_linux_amd64_v1.0.0", name) {
- t.Fatalf("want %v, got %v", "core-build_linux_amd64_v1.0.0", name)
- }
-
- })
-}
-
-func TestCi_WriteArtifactMeta_Good(t *testing.T) {
- fs := storage.Local
-
- t.Run("writes valid JSON with CI context", func(t *testing.T) {
- dir := t.TempDir()
- path := ax.Join(dir, "artifact_meta.json")
-
- ci := &CIContext{
- Ref: "refs/tags/v1.2.3",
- SHA: "abc1234def5678",
- ShortSHA: "abc1234",
- Tag: "v1.2.3",
- IsTag: true,
- Repo: "dappcore/core",
- Owner: "dappcore",
- }
-
- requireCIOK(t, WriteArtifactMeta(fs, path, "core", Target{OS: "linux", Arch: "amd64"}, ci))
-
- content := requireCIBytes(t, ax.ReadFile(path))
-
- var meta map[string]any
- requireCIOK(t, ax.JSONUnmarshal(content, &meta))
- if !stdlibAssertEqual("core", meta["name"]) {
- t.Fatalf("want %v, got %v", "core", meta["name"])
- }
- if !stdlibAssertEqual("linux", meta[artifactMetaOSField]) {
- t.Fatalf("want %v, got %v", "linux", meta[artifactMetaOSField])
- }
- if !stdlibAssertEqual("amd64", meta["arch"]) {
- t.Fatalf("want %v, got %v", "amd64", meta["arch"])
- }
- if !stdlibAssertEqual("v1.2.3", meta["tag"]) {
- t.Fatalf("want %v, got %v", "v1.2.3", meta["tag"])
- }
- if !stdlibAssertEqual(true, meta["is_tag"]) {
- t.Fatalf("want %v, got %v", true, meta["is_tag"])
- }
- if !stdlibAssertEqual("dappcore/core", meta["repo"]) {
- t.Fatalf("want %v, got %v", "dappcore/core", meta["repo"])
- }
- if !stdlibAssertEqual("refs/tags/v1.2.3", meta["ref"]) {
- t.Fatalf("want %v, got %v", "refs/tags/v1.2.3", meta["ref"])
- }
-
- })
-
- t.Run("writes valid JSON without CI context", func(t *testing.T) {
- dir := t.TempDir()
- path := ax.Join(dir, "artifact_meta.json")
-
- requireCIOK(t, WriteArtifactMeta(fs, path, "myapp", Target{OS: "darwin", Arch: "arm64"}, nil))
-
- content := requireCIBytes(t, ax.ReadFile(path))
-
- var meta map[string]any
- requireCIOK(t, ax.JSONUnmarshal(content, &meta))
- if !stdlibAssertEqual("myapp", meta["name"]) {
- t.Fatalf("want %v, got %v", "myapp", meta["name"])
- }
- if !stdlibAssertEqual("darwin", meta[artifactMetaOSField]) {
- t.Fatalf("want %v, got %v", "darwin", meta[artifactMetaOSField])
- }
- if !stdlibAssertEqual("arm64", meta["arch"]) {
- t.Fatalf("want %v, got %v", "arm64", meta["arch"])
- }
- if !stdlibAssertEqual(false, meta["is_tag"]) {
- t.Fatalf("want %v, got %v", false, meta["is_tag"])
- }
-
- })
-
- t.Run("output is pretty-printed JSON", func(t *testing.T) {
- dir := t.TempDir()
- path := ax.Join(dir, "artifact_meta.json")
-
- requireCIOK(t, WriteArtifactMeta(fs, path, "core", Target{OS: "windows", Arch: "amd64"}, nil))
-
- content := requireCIBytes(t, ax.ReadFile(path))
- if !stdlibAssertContains(string(content), "\n") {
- t.Fatalf("expected %v to contain %v", string(content), "\n")
- }
- if !stdlibAssertContains(string(content), " ") {
- t.Fatalf("expected %v to contain %v", string(content), " ")
- }
-
- })
-}
-
-func TestCi_CIArtifactPath_Good(t *testing.T) {
- t.Run("stamps tar.gz artifacts with tag names", func(t *testing.T) {
- ci := &CIContext{
- IsTag: true,
- Tag: "v1.2.3",
- ShortSHA: "abc1234",
- }
-
- path := CIArtifactPath("core", ci, Artifact{
- Path: "/tmp/dist/linux_amd64/core.tar.gz",
- OS: "linux",
- Arch: "amd64",
- })
- if !stdlibAssertEqual("/tmp/dist/linux_amd64/core_linux_amd64_v1.2.3.tar.gz", path) {
- t.Fatalf("want %v, got %v", "/tmp/dist/linux_amd64/core_linux_amd64_v1.2.3.tar.gz", path)
- }
-
- })
-
- t.Run("stamps app bundles without losing the bundle suffix", func(t *testing.T) {
- ci := &CIContext{
- IsTag: false,
- ShortSHA: "abc1234",
- }
-
- path := CIArtifactPath("core", ci, Artifact{
- Path: "/tmp/dist/darwin_arm64/Core.app",
- OS: "darwin",
- Arch: "arm64",
- })
- if !stdlibAssertEqual("/tmp/dist/darwin_arm64/core_darwin_arm64_abc1234.app", path) {
- t.Fatalf("want %v, got %v", "/tmp/dist/darwin_arm64/core_darwin_arm64_abc1234.app", path)
- }
-
- })
-
- t.Run("returns the original path when CI metadata is unavailable", func(t *testing.T) {
- artifact := Artifact{
- Path: "/tmp/dist/linux_amd64/core",
- OS: "linux",
- Arch: "amd64",
- }
- if !stdlibAssertEqual(artifact.Path, CIArtifactPath("core", nil, artifact)) {
- t.Fatalf("want %v, got %v", artifact.Path, CIArtifactPath("core", nil, artifact))
- }
-
- })
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCi_DetectGitHubMetadata_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DetectGitHubMetadata()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCi_DetectGitHubMetadata_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DetectGitHubMetadata()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCi_WriteArtifactMeta_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteArtifactMeta(storage.NewMemoryMedium(), "", "", Target{OS: "linux", Arch: "amd64"}, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCi_WriteArtifactMeta_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteArtifactMeta(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), "agent", Target{OS: "linux", Arch: "amd64"}, &CIContext{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCi_CIArtifactPath_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CIArtifactPath("", nil, Artifact{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCi_CIArtifactPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CIArtifactPath("agent", &CIContext{}, Artifact{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/config.go b/pkg/build/config.go
deleted file mode 100644
index 27ec74b..0000000
--- a/pkg/build/config.go
+++ /dev/null
@@ -1,1064 +0,0 @@
-// Package build provides project type detection and cross-compilation for the Core build system.
-// This file handles configuration loading from .core/build.yaml files.
-package build
-
-import (
- "iter"
- "reflect"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build/signing"
- "dappco.re/go/build/pkg/sdk"
- storage "dappco.re/go/build/pkg/storage"
- "gopkg.in/yaml.v3" // Note: AX-6 — no core YAMLUnmarshal yet.
-)
-
-// ConfigFileName is the name of the build configuration file.
-//
-// configPath := ax.Join(projectDir, build.ConfigDir, build.ConfigFileName)
-const ConfigFileName = "build.yaml"
-
-// ConfigDir is the directory where build configuration is stored.
-//
-// configPath := ax.Join(projectDir, build.ConfigDir, build.ConfigFileName)
-const ConfigDir = ".core"
-
-// BuildConfig holds the complete build configuration loaded from .core/build.yaml.
-// This is distinct from Config which holds runtime build parameters.
-//
-// cfg, err := build.LoadConfig(storage.Local, ".")
-type BuildConfig struct {
- // Version is the config file format version.
- Version int `json:"version" yaml:"version"`
- // Project contains project metadata.
- Project Project `json:"project" yaml:"project"`
- // Build contains build settings.
- Build Build `json:"build" yaml:"build"`
- // Apple contains macOS Apple pipeline settings.
- Apple AppleConfig `json:"apple,omitempty" yaml:"apple,omitempty"`
- // PreBuild contains declarative frontend build hooks such as Deno or npm.
- PreBuild PreBuild `json:"pre_build,omitempty" yaml:"pre_build,omitempty"`
- // Targets defines the build targets.
- Targets []TargetConfig `json:"targets" yaml:"targets"`
- // Sign contains code signing configuration.
- Sign signing.SignConfig `json:"sign,omitempty" yaml:"sign,omitempty"`
- // SDK contains OpenAPI SDK generation configuration.
- SDK *sdk.Config `json:"sdk,omitempty" yaml:"sdk,omitempty"`
- // LinuxKit contains immutable image configuration for `core build image`.
- LinuxKit LinuxKitConfig `json:"linuxkit,omitempty" yaml:"linuxkit,omitempty"`
-}
-
-type rawSignConfig struct {
- Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
- GPG signing.GPGConfig `json:"gpg,omitempty" yaml:"gpg,omitempty"`
- MacOS signing.MacOSConfig `json:"macos,omitempty" yaml:"macos,omitempty"`
- Windows rawWindowsSignConfig `json:"windows,omitempty" yaml:"windows,omitempty"`
-}
-
-type rawWindowsSignConfig struct {
- Signtool *bool `json:"signtool,omitempty" yaml:"signtool,omitempty"`
- Certificate string `json:"certificate,omitempty" yaml:"certificate,omitempty"`
- Password string `json:"password,omitempty" yaml:"password,omitempty"`
-}
-
-// Project holds project metadata.
-//
-// cfg.Project.Binary = "core-build"
-type Project struct {
- // Name is the project name.
- Name string `json:"name" yaml:"name"`
- // Description is a brief description of the project.
- Description string `json:"description" yaml:"description"`
- // Main is the path to the main package (e.g., ./cmd/core).
- Main string `json:"main" yaml:"main"`
- // Binary is the output binary name.
- Binary string `json:"binary" yaml:"binary"`
-}
-
-// Build holds build-time settings.
-//
-// cfg.Build.LDFlags = []string{"-s", "-w", "-X main.version=" + version}
-type Build struct {
- // Type overrides project type auto-detection (e.g., "go", "wails", "docker").
- Type string `json:"type" yaml:"type"`
- // CGO enables CGO for the build.
- CGO bool `json:"cgo" yaml:"cgo"`
- // Obfuscate uses garble instead of go build for binary obfuscation.
- Obfuscate bool `json:"obfuscate" yaml:"obfuscate"`
- // DenoBuild overrides the default `deno task build` invocation for Deno-backed builds.
- DenoBuild string `json:"deno_build,omitempty" yaml:"deno_build,omitempty"`
- // NpmBuild overrides the default `npm run build` invocation for npm-backed builds.
- NpmBuild string `json:"npm_build,omitempty" yaml:"npm_build,omitempty"`
- // NSIS enables Windows NSIS installer generation (Wails projects only).
- NSIS bool `json:"nsis" yaml:"nsis"`
- // WebView2 sets the WebView2 delivery method: download|embed|browser|error.
- WebView2 string `json:"webview2,omitempty" yaml:"webview2,omitempty"`
- // Flags are additional build flags (e.g., ["-trimpath"]).
- Flags []string `json:"flags" yaml:"flags"`
- // LDFlags are linker flags (e.g., ["-s", "-w"]).
- LDFlags []string `json:"ldflags" yaml:"ldflags"`
- // BuildTags are Go build tags passed through to `go build`.
- BuildTags []string `json:"build_tags,omitempty" yaml:"build_tags,omitempty"`
- // ArchiveFormat selects the archive compression format for build outputs.
- // Supported values are "gz", "xz", and "zip"; empty uses gzip.
- ArchiveFormat string `json:"archive_format,omitempty" yaml:"archive_format,omitempty"`
- // Env are additional environment variables.
- Env []string `json:"env" yaml:"env"`
- // Cache controls build cache setup.
- Cache CacheConfig `json:"cache,omitempty" yaml:"cache,omitempty"`
- // Dockerfile is the path to the Dockerfile used by Docker builds.
- Dockerfile string `json:"dockerfile,omitempty" yaml:"dockerfile,omitempty"`
- // Registry is the container registry used for Docker image references.
- Registry string `json:"registry,omitempty" yaml:"registry,omitempty"`
- // Image is the image name used for Docker builds.
- Image string `json:"image,omitempty" yaml:"image,omitempty"`
- // Tags are Docker image tags to apply.
- Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
- // BuildArgs are Docker build arguments.
- BuildArgs map[string]string `json:"build_args,omitempty" yaml:"build_args,omitempty"`
- // Push enables pushing Docker images after build.
- Push bool `json:"push,omitempty" yaml:"push,omitempty"`
- // Load loads a single-platform Docker image into the local daemon after build.
- Load bool `json:"load,omitempty" yaml:"load,omitempty"`
- // LinuxKitConfig is the path to the LinuxKit config file.
- LinuxKitConfig string `json:"linuxkit_config,omitempty" yaml:"linuxkit_config,omitempty"`
- // Formats is the list of LinuxKit output formats.
- // Supported values include iso, raw, qcow2, vmdk, vhd, gcp, aws, docker, tar, and kernel+initrd.
- Formats []string `json:"formats,omitempty" yaml:"formats,omitempty"`
-}
-
-// PreBuild holds declarative frontend build hooks loaded from the RFC
-// `pre_build:` block.
-//
-// cfg.PreBuild = build.PreBuild{Deno: "deno task build", Npm: "npm run build"}
-type PreBuild struct {
- // Deno overrides the default `deno task build` invocation.
- Deno string `json:"deno,omitempty" yaml:"deno,omitempty"`
- // Npm overrides the default `npm run build` invocation.
- Npm string `json:"npm,omitempty" yaml:"npm,omitempty"`
-}
-
-// AppleConfig holds macOS Apple pipeline settings loaded from .core/build.yaml.
-// Pointer booleans preserve the difference between an explicit false and an unset field.
-type AppleConfig struct {
- TeamID string `json:"team_id,omitempty" yaml:"team_id,omitempty"`
- BundleID string `json:"bundle_id,omitempty" yaml:"bundle_id,omitempty"`
- Arch string `json:"arch,omitempty" yaml:"arch,omitempty"`
- CertIdentity string `json:"cert_identity,omitempty" yaml:"cert_identity,omitempty"`
- ProfilePath string `json:"profile_path,omitempty" yaml:"profile_path,omitempty"`
- KeychainPath string `json:"keychain_path,omitempty" yaml:"keychain_path,omitempty"`
- MetadataPath string `json:"metadata_path,omitempty" yaml:"metadata_path,omitempty"`
-
- Sign *bool `json:"sign,omitempty" yaml:"sign,omitempty"`
- Notarise *bool `json:"notarise,omitempty" yaml:"notarise,omitempty"`
- DMG *bool `json:"dmg,omitempty" yaml:"dmg,omitempty"`
- TestFlight *bool `json:"testflight,omitempty" yaml:"testflight,omitempty"`
- AppStore *bool `json:"appstore,omitempty" yaml:"appstore,omitempty"`
-
- APIKeyID string `json:"api_key_id,omitempty" yaml:"api_key_id,omitempty"`
- APIKeyIssuerID string `json:"api_key_issuer_id,omitempty" yaml:"api_key_issuer_id,omitempty"`
- APIKeyPath string `json:"api_key_path,omitempty" yaml:"api_key_path,omitempty"`
- AppleID string `json:"apple_id,omitempty" yaml:"apple_id,omitempty"`
- Password string `json:"password,omitempty" yaml:"password,omitempty"`
-
- BundleDisplayName string `json:"bundle_display_name,omitempty" yaml:"bundle_display_name,omitempty"`
- MinSystemVersion string `json:"min_system_version,omitempty" yaml:"min_system_version,omitempty"`
- Category string `json:"category,omitempty" yaml:"category,omitempty"`
- Copyright string `json:"copyright,omitempty" yaml:"copyright,omitempty"`
- PrivacyPolicyURL string `json:"privacy_policy_url,omitempty" yaml:"privacy_policy_url,omitempty"`
- DMGBackground string `json:"dmg_background,omitempty" yaml:"dmg_background,omitempty"`
- DMGVolumeName string `json:"dmg_volume_name,omitempty" yaml:"dmg_volume_name,omitempty"`
- EntitlementsPath string `json:"entitlements_path,omitempty" yaml:"entitlements_path,omitempty"`
- XcodeCloud XcodeCloudConfig `json:"xcode_cloud,omitempty" yaml:"xcode_cloud,omitempty"`
-}
-
-// XcodeCloudConfig defines the Xcode Cloud workflow metadata stored in build config.
-type XcodeCloudConfig struct {
- Workflow string `json:"workflow,omitempty" yaml:"workflow,omitempty"`
- Triggers []XcodeCloudTrigger `json:"triggers,omitempty" yaml:"triggers,omitempty"`
-}
-
-// XcodeCloudTrigger defines a single Xcode Cloud trigger rule.
-type XcodeCloudTrigger struct {
- Branch string `json:"branch,omitempty" yaml:"branch,omitempty"`
- Tag string `json:"tag,omitempty" yaml:"tag,omitempty"`
- Action string `json:"action,omitempty" yaml:"action,omitempty"`
-}
-
-// TargetConfig defines a build target in the config file.
-// This is separate from Target to allow for additional config-specific fields.
-//
-// cfg.Targets = []build.TargetConfig{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}
-type TargetConfig struct {
- // OS is the target operating system (e.g., "linux", "darwin", "windows").
- OS string
- // Arch is the target architecture (e.g., "amd64", "arm64").
- Arch string `json:"arch" yaml:"arch"`
-}
-
-const targetConfigOSField = "o" + "s"
-
-func (t TargetConfig) MarshalYAML() core.Result {
- return core.Ok(map[string]string{
- targetConfigOSField: t.OS,
- "arch": t.Arch,
- })
-}
-
-func (t *TargetConfig) UnmarshalYAML(value *yaml.Node) core.Result {
- var raw map[string]string
- if err := value.Decode(&raw); err != nil {
- return core.Fail(err)
- }
- t.OS = raw[targetConfigOSField]
- t.Arch = raw["arch"]
- return core.Ok(nil)
-}
-
-type buildConfigYAML struct {
- Version int `json:"version" yaml:"version"`
- Project Project `json:"project" yaml:"project"`
- Build buildYAML `json:"build" yaml:"build"`
- Cache *CacheConfig `json:"cache,omitempty" yaml:"cache,omitempty"`
- Apple AppleConfig `json:"apple,omitempty" yaml:"apple,omitempty"`
- PreBuild *PreBuild `json:"pre_build,omitempty" yaml:"pre_build,omitempty"`
- Targets []TargetConfig `json:"targets" yaml:"targets"`
- Sign signing.SignConfig `json:"sign,omitempty" yaml:"sign,omitempty"`
- SDK *sdk.Config `json:"sdk,omitempty" yaml:"sdk,omitempty"`
- LinuxKit LinuxKitConfig `json:"linuxkit,omitempty" yaml:"linuxkit,omitempty"`
-}
-
-type buildYAML struct {
- Type string `json:"type" yaml:"type"`
- CGO bool `json:"cgo" yaml:"cgo"`
- Obfuscate bool `json:"obfuscate" yaml:"obfuscate"`
- DenoBuild string `json:"deno_build,omitempty" yaml:"deno_build,omitempty"`
- NpmBuild string `json:"npm_build,omitempty" yaml:"npm_build,omitempty"`
- NSIS bool `json:"nsis" yaml:"nsis"`
- WebView2 string `json:"webview2,omitempty" yaml:"webview2,omitempty"`
- Flags []string `json:"flags" yaml:"flags"`
- LDFlags []string `json:"ldflags" yaml:"ldflags"`
- BuildTags []string `json:"build_tags,omitempty" yaml:"build_tags,omitempty"`
- ArchiveFormat string `json:"archive_format,omitempty" yaml:"archive_format,omitempty"`
- Env []string `json:"env" yaml:"env"`
- Dockerfile string `json:"dockerfile,omitempty" yaml:"dockerfile,omitempty"`
- Registry string `json:"registry,omitempty" yaml:"registry,omitempty"`
- Image string `json:"image,omitempty" yaml:"image,omitempty"`
- Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
- BuildArgs map[string]string `json:"build_args,omitempty" yaml:"build_args,omitempty"`
- Push bool `json:"push,omitempty" yaml:"push,omitempty"`
- Load bool `json:"load,omitempty" yaml:"load,omitempty"`
- LinuxKitConfig string `json:"linuxkit_config,omitempty" yaml:"linuxkit_config,omitempty"`
- Formats []string `json:"formats,omitempty" yaml:"formats,omitempty"`
-}
-
-// UnmarshalYAML accepts both the documented top-level `cache:` block and the
-// legacy nested `build.cache:` shape. When both are present, the nested
-// `build.cache` form wins to preserve compatibility with existing callers.
-func (cfg *BuildConfig) UnmarshalYAML(value *yaml.Node) core.Result {
- type rawBuildConfig struct {
- Version int `json:"version" yaml:"version"`
- Project Project `json:"project" yaml:"project"`
- Build Build `json:"build" yaml:"build"`
- Cache CacheConfig `json:"cache,omitempty" yaml:"cache,omitempty"`
- Apple AppleConfig `json:"apple,omitempty" yaml:"apple,omitempty"`
- PreBuild PreBuild `json:"pre_build,omitempty" yaml:"pre_build,omitempty"`
- Targets []TargetConfig `json:"targets" yaml:"targets"`
- Sign *rawSignConfig `json:"sign,omitempty" yaml:"sign,omitempty"`
- SDK yaml.Node `json:"sdk,omitempty" yaml:"sdk,omitempty"`
- LinuxKit LinuxKitConfig `json:"linuxkit,omitempty" yaml:"linuxkit,omitempty"`
- }
-
- var raw rawBuildConfig
- if err := value.Decode(&raw); err != nil {
- return core.Fail(err)
- }
-
- *cfg = BuildConfig{
- Version: raw.Version,
- Project: raw.Project,
- Build: raw.Build,
- Apple: raw.Apple,
- PreBuild: raw.PreBuild,
- Targets: raw.Targets,
- LinuxKit: raw.LinuxKit,
- }
- if raw.SDK.Kind != 0 {
- sdkConfigResult := decodeBuildSDKConfig(&raw.SDK)
- if !sdkConfigResult.OK {
- return sdkConfigResult
- }
- cfg.SDK = sdkConfigResult.Value.(*sdk.Config)
- }
-
- // Accept the RFC-shaped top-level pre_build block while preserving the
- // legacy build.deno_build and build.npm_build fields when both are present.
- if cfg.Build.DenoBuild == "" {
- cfg.Build.DenoBuild = cfg.PreBuild.Deno
- }
- if cfg.Build.NpmBuild == "" {
- cfg.Build.NpmBuild = cfg.PreBuild.Npm
- }
- cfg.PreBuild = PreBuild{
- Deno: cfg.Build.DenoBuild,
- Npm: cfg.Build.NpmBuild,
- }
-
- if !cacheConfigConfigured(cfg.Build.Cache) && cacheConfigConfigured(raw.Cache) {
- cfg.Build.Cache = raw.Cache
- }
- cfg.Sign = mergeSignConfig(raw.Sign)
-
- return core.Ok(nil)
-}
-
-func decodeBuildSDKConfig(node *yaml.Node) core.Result {
- if node == nil {
- return core.Ok((*sdk.Config)(nil))
- }
-
- working := cloneYAMLNode(node)
- diffConfigured, diffEnabled, scalarDiff := normalizeBuildSDKDiffNode(&working)
-
- var config sdk.Config
- if failure := working.Decode(&config); failure != nil {
- return core.Fail(failure)
- }
- if diffConfigured {
- config.Diff.EnabledConfigured = true
- if scalarDiff {
- config.Diff.Enabled = diffEnabled
- }
- }
-
- return core.Ok(&config)
-}
-
-func cloneYAMLNode(node *yaml.Node) yaml.Node {
- if node == nil {
- return yaml.Node{}
- }
-
- clone := *node
- if len(node.Content) > 0 {
- clone.Content = make([]*yaml.Node, len(node.Content))
- for i, child := range node.Content {
- childClone := cloneYAMLNode(child)
- clone.Content[i] = &childClone
- }
- }
- return clone
-}
-
-func normalizeBuildSDKDiffNode(node *yaml.Node) (bool, bool, bool) {
- if node == nil || node.Kind != yaml.MappingNode {
- return false, false, false
- }
-
- for i := 0; i+1 < len(node.Content); i += 2 {
- key := node.Content[i]
- value := node.Content[i+1]
- if key == nil || value == nil || key.Value != "diff" {
- continue
- }
- if value.Kind == yaml.MappingNode {
- return buildSDKDiffMappingHasEnabled(value), false, false
- }
- if value.Kind != yaml.ScalarNode {
- return false, false, false
- }
-
- enabled := value.Value == "true" || value.Value == "True" || value.Value == "TRUE"
- boolValue := "false"
- if enabled {
- boolValue = "true"
- }
- node.Content[i+1] = &yaml.Node{
- Kind: yaml.MappingNode,
- Content: []*yaml.Node{
- {Kind: yaml.ScalarNode, Tag: "!!str", Value: "enabled"},
- {Kind: yaml.ScalarNode, Tag: "!!bool", Value: boolValue},
- },
- }
- return true, enabled, true
- }
-
- return false, false, false
-}
-
-func buildSDKDiffMappingHasEnabled(node *yaml.Node) bool {
- if node == nil || node.Kind != yaml.MappingNode {
- return false
- }
- for i := 0; i+1 < len(node.Content); i += 2 {
- key := node.Content[i]
- if key != nil && key.Value == "enabled" {
- return true
- }
- }
- return false
-}
-
-// MarshalYAML emits the documented `.core/build.yaml` shape, including the
-// top-level `cache:` block, while continuing to use Build.Cache internally.
-func (cfg BuildConfig) MarshalYAML() core.Result {
- raw := buildConfigYAML{
- Version: cfg.Version,
- Project: cfg.Project,
- Build: buildYAMLFromBuild(cfg.Build),
- Apple: cfg.Apple,
- Targets: cfg.Targets,
- Sign: cfg.Sign,
- SDK: cfg.SDK,
- LinuxKit: cfg.LinuxKit,
- }
-
- if preBuildConfigured(cfg.PreBuild) {
- preBuild := cfg.PreBuild
- raw.PreBuild = &preBuild
- } else if cfg.Build.DenoBuild != "" || cfg.Build.NpmBuild != "" {
- raw.PreBuild = &PreBuild{
- Deno: cfg.Build.DenoBuild,
- Npm: cfg.Build.NpmBuild,
- }
- }
-
- if cacheConfigConfigured(cfg.Build.Cache) {
- cache := cfg.Build.Cache
- cache.Dir = cache.effectiveDirectory()
- cache.Directory = cache.Dir
- raw.Cache = &cache
- }
-
- return core.Ok(raw)
-}
-
-func buildYAMLFromBuild(value Build) buildYAML {
- return buildYAML{
- Type: value.Type,
- CGO: value.CGO,
- Obfuscate: value.Obfuscate,
- NSIS: value.NSIS,
- WebView2: value.WebView2,
- Flags: value.Flags,
- LDFlags: value.LDFlags,
- BuildTags: value.BuildTags,
- ArchiveFormat: value.ArchiveFormat,
- Env: value.Env,
- Dockerfile: value.Dockerfile,
- Registry: value.Registry,
- Image: value.Image,
- Tags: value.Tags,
- BuildArgs: value.BuildArgs,
- Push: value.Push,
- Load: value.Load,
- LinuxKitConfig: value.LinuxKitConfig,
- Formats: value.Formats,
- }
-}
-
-// LoadConfig loads build configuration from the .core/build.yaml file in the given directory.
-// If the config file does not exist, it returns DefaultConfig().
-// Returns an error if the file exists but cannot be parsed.
-//
-// cfg, err := build.LoadConfig(storage.Local, ".")
-func LoadConfig(fs storage.Medium, dir string) core.Result {
- if fs == nil {
- fs = storage.Local
- }
- return LoadConfigAtPath(fs, ax.Join(dir, ConfigDir, ConfigFileName))
-}
-
-// LoadConfigAtPath loads build configuration from an explicit file path.
-// If the file does not exist, it returns DefaultConfig().
-// Returns an error if the file exists but cannot be parsed.
-//
-// cfg, err := build.LoadConfigAtPath(storage.Local, "/tmp/project/build.yaml")
-func LoadConfigAtPath(fs storage.Medium, configPath string) core.Result {
- if fs == nil {
- fs = storage.Local
- }
-
- content := fs.Read(configPath)
- if !content.OK {
- if !fs.Exists(configPath) {
- return core.Ok(DefaultConfig())
- }
- return core.Fail(core.E("build.LoadConfigAtPath", "failed to read config file", core.NewError(content.Error())))
- }
-
- cfg := DefaultConfig()
- var node yaml.Node
- if err := yaml.Unmarshal([]byte(content.Value.(string)), &node); err != nil {
- return core.Fail(core.E("build.LoadConfigAtPath", "failed to parse config file", err))
- }
- loaded := cfg.UnmarshalYAML(&node)
- if !loaded.OK {
- return core.Fail(core.E("build.LoadConfigAtPath", "failed to parse config file", core.NewError(loaded.Error())))
- }
-
- // Apply defaults for any missing fields
- applyDefaults(cfg)
-
- // Expand environment variables after defaults so overrides can still be
- // expressed declaratively in config files.
- cfg.ExpandEnv()
- if cfg.SDK != nil {
- cfg.SDK.ApplyDefaults()
- }
-
- return core.Ok(cfg)
-}
-
-// DefaultConfig returns sensible defaults for Go projects.
-//
-// cfg := build.DefaultConfig()
-func DefaultConfig() *BuildConfig {
- return &BuildConfig{
- Version: 1,
- Project: Project{
- Name: "",
- Main: ".",
- Binary: "",
- },
- Build: Build{
- CGO: false,
- Flags: []string{"-trimpath"},
- LDFlags: []string{"-s", "-w"},
- Env: []string{},
- },
- Targets: defaultTargetConfigs(),
- Sign: signing.DefaultSignConfig(),
- LinuxKit: DefaultLinuxKitConfig(),
- }
-}
-
-// ResolveOutputMedium returns the artifact output medium for a runtime build
-// config, falling back to storage.Local when no explicit medium was provided.
-func ResolveOutputMedium(cfg *Config) storage.Medium {
- if cfg == nil || cfg.OutputMedium == nil {
- return storage.Local
- }
- return cfg.OutputMedium
-}
-
-// MediumIsLocal reports whether a medium is the package-level local filesystem.
-func MediumIsLocal(medium storage.Medium) bool {
- return outputMediumEquals(medium, storage.Local)
-}
-
-func outputMediumEquals(left, right storage.Medium) bool {
- if left == nil || right == nil {
- return left == nil && right == nil
- }
-
- leftType := reflect.TypeOf(left)
- rightType := reflect.TypeOf(right)
- if leftType != rightType || !leftType.Comparable() {
- return false
- }
-
- return reflect.ValueOf(left).Interface() == reflect.ValueOf(right).Interface()
-}
-
-// CopyMediumPath copies a file or directory tree between media while preserving
-// file modes where the source medium exposes them.
-func CopyMediumPath(source storage.Medium, sourcePath string, destination storage.Medium, destinationPath string) core.Result {
- if source == nil {
- source = storage.Local
- }
- if destination == nil {
- destination = storage.Local
- }
-
- info := source.Stat(sourcePath)
- if !info.OK {
- return core.Fail(core.E("build.CopyMediumPath", "failed to stat source path "+sourcePath, core.NewError(info.Error())))
- }
- fileInfo := info.Value.(core.FsFileInfo)
-
- if fileInfo.IsDir() {
- return copyMediumDirectory(source, sourcePath, destination, destinationPath)
- }
-
- destinationDir := ax.Dir(destinationPath)
- if destinationDir != "" && destinationDir != "." {
- created := destination.EnsureDir(destinationDir)
- if !created.OK {
- return core.Fail(core.E("build.CopyMediumPath", "failed to create destination directory", core.NewError(created.Error())))
- }
- }
-
- content := source.Read(sourcePath)
- if !content.OK {
- return core.Fail(core.E("build.CopyMediumPath", "failed to read source file "+sourcePath, core.NewError(content.Error())))
- }
-
- written := destination.WriteMode(destinationPath, content.Value.(string), fileInfo.Mode())
- if !written.OK {
- return core.Fail(core.E("build.CopyMediumPath", "failed to write destination file "+destinationPath, core.NewError(written.Error())))
- }
- return core.Ok(nil)
-}
-
-func copyMediumDirectory(source storage.Medium, sourcePath string, destination storage.Medium, destinationPath string) core.Result {
- if destinationPath != "" && destinationPath != "." {
- created := destination.EnsureDir(destinationPath)
- if !created.OK {
- return core.Fail(core.E("build.CopyMediumPath", "failed to create destination directory "+destinationPath, core.NewError(created.Error())))
- }
- }
-
- entries := source.List(sourcePath)
- if !entries.OK {
- return core.Fail(core.E("build.CopyMediumPath", "failed to list source directory "+sourcePath, core.NewError(entries.Error())))
- }
-
- for _, entry := range entries.Value.([]core.FsDirEntry) {
- childSourcePath := ax.Join(sourcePath, entry.Name())
- childDestinationPath := ax.Join(destinationPath, entry.Name())
- copied := CopyMediumPath(source, childSourcePath, destination, childDestinationPath)
- if !copied.OK {
- return copied
- }
- }
- return core.Ok(nil)
-}
-
-func defaultTargetConfigs() []TargetConfig {
- return []TargetConfig{
- {OS: "linux", Arch: "amd64"},
- {OS: "linux", Arch: "arm64"},
- {OS: "darwin", Arch: "amd64"},
- {OS: "darwin", Arch: "arm64"},
- {OS: "windows", Arch: "amd64"},
- }
-}
-
-// applyDefaults fills in default values for any empty fields in the config.
-func applyDefaults(cfg *BuildConfig) {
- defaults := DefaultConfig()
-
- if cfg.Version == 0 {
- cfg.Version = defaults.Version
- }
-
- if cfg.Project.Main == "" {
- cfg.Project.Main = defaults.Project.Main
- }
-
- if cfg.Build.Flags == nil {
- cfg.Build.Flags = defaults.Build.Flags
- }
-
- if cfg.Build.LDFlags == nil {
- cfg.Build.LDFlags = defaults.Build.LDFlags
- }
-
- if cfg.Build.Env == nil {
- cfg.Build.Env = defaults.Build.Env
- }
-
- if cfg.Targets == nil {
- cfg.Targets = append([]TargetConfig(nil), defaults.Targets...)
- }
-
- cfg.LinuxKit = applyLinuxKitDefaults(cfg.LinuxKit)
-}
-
-func cacheConfigConfigured(cfg CacheConfig) bool {
- return cfg.Enabled ||
- cfg.Dir != "" ||
- cfg.Directory != "" ||
- cfg.KeyPrefix != "" ||
- len(cfg.Paths) > 0 ||
- len(cfg.RestoreKeys) > 0
-}
-
-func preBuildConfigured(cfg PreBuild) bool {
- return cfg.Deno != "" || cfg.Npm != ""
-}
-
-func mergeSignConfig(raw *rawSignConfig) signing.SignConfig {
- cfg := signing.DefaultSignConfig()
- if raw == nil {
- return cfg
- }
-
- if raw.Enabled != nil {
- cfg.Enabled = *raw.Enabled
- }
- if raw.GPG.Key != "" {
- cfg.GPG.Key = raw.GPG.Key
- }
- if raw.MacOS.Identity != "" {
- cfg.MacOS.Identity = raw.MacOS.Identity
- }
- cfg.MacOS.Notarize = raw.MacOS.Notarize
- if raw.MacOS.AppleID != "" {
- cfg.MacOS.AppleID = raw.MacOS.AppleID
- }
- if raw.MacOS.TeamID != "" {
- cfg.MacOS.TeamID = raw.MacOS.TeamID
- }
- if raw.MacOS.AppPassword != "" {
- cfg.MacOS.AppPassword = raw.MacOS.AppPassword
- }
- if raw.Windows.Certificate != "" {
- cfg.Windows.Certificate = raw.Windows.Certificate
- }
- if raw.Windows.Password != "" {
- cfg.Windows.Password = raw.Windows.Password
- }
- if raw.Windows.Signtool != nil {
- cfg.Windows.SetSigntool(*raw.Windows.Signtool)
- }
-
- return cfg
-}
-
-// ExpandEnv expands environment variables across the build config.
-//
-// cfg.ExpandEnv() // expands $APP_NAME, $IMAGE_TAG, $GPG_KEY_ID, etc.
-func (cfg *BuildConfig) ExpandEnv() {
- if cfg == nil {
- return
- }
-
- cfg.Project.Name = expandEnv(cfg.Project.Name)
- cfg.Project.Description = expandEnv(cfg.Project.Description)
- cfg.Project.Main = expandEnv(cfg.Project.Main)
- cfg.Project.Binary = expandEnv(cfg.Project.Binary)
-
- cfg.Build.Type = expandEnv(cfg.Build.Type)
- cfg.Build.DenoBuild = expandEnv(cfg.Build.DenoBuild)
- cfg.Build.NpmBuild = expandEnv(cfg.Build.NpmBuild)
- cfg.Build.WebView2 = expandEnv(cfg.Build.WebView2)
- cfg.Build.ArchiveFormat = expandEnv(cfg.Build.ArchiveFormat)
- cfg.Build.Dockerfile = expandEnv(cfg.Build.Dockerfile)
- cfg.Build.Registry = expandEnv(cfg.Build.Registry)
- cfg.Build.Image = expandEnv(cfg.Build.Image)
- cfg.Build.LinuxKitConfig = core.Trim(expandEnv(cfg.Build.LinuxKitConfig))
-
- cfg.Apple.TeamID = expandEnv(cfg.Apple.TeamID)
- cfg.Apple.BundleID = expandEnv(cfg.Apple.BundleID)
- cfg.Apple.Arch = expandEnv(cfg.Apple.Arch)
- cfg.Apple.CertIdentity = expandEnv(cfg.Apple.CertIdentity)
- cfg.Apple.ProfilePath = expandEnv(cfg.Apple.ProfilePath)
- cfg.Apple.KeychainPath = expandEnv(cfg.Apple.KeychainPath)
- cfg.Apple.MetadataPath = expandEnv(cfg.Apple.MetadataPath)
- cfg.Apple.APIKeyID = expandEnv(cfg.Apple.APIKeyID)
- cfg.Apple.APIKeyIssuerID = expandEnv(cfg.Apple.APIKeyIssuerID)
- cfg.Apple.APIKeyPath = expandEnv(cfg.Apple.APIKeyPath)
- cfg.Apple.AppleID = expandEnv(cfg.Apple.AppleID)
- cfg.Apple.Password = expandEnv(cfg.Apple.Password)
- cfg.Apple.BundleDisplayName = expandEnv(cfg.Apple.BundleDisplayName)
- cfg.Apple.MinSystemVersion = expandEnv(cfg.Apple.MinSystemVersion)
- cfg.Apple.Category = expandEnv(cfg.Apple.Category)
- cfg.Apple.Copyright = expandEnv(cfg.Apple.Copyright)
- cfg.Apple.PrivacyPolicyURL = expandEnv(cfg.Apple.PrivacyPolicyURL)
- cfg.Apple.DMGBackground = expandEnv(cfg.Apple.DMGBackground)
- cfg.Apple.DMGVolumeName = expandEnv(cfg.Apple.DMGVolumeName)
- cfg.Apple.EntitlementsPath = expandEnv(cfg.Apple.EntitlementsPath)
- cfg.Apple.XcodeCloud.Workflow = expandEnv(cfg.Apple.XcodeCloud.Workflow)
- cfg.PreBuild.Deno = expandEnv(cfg.PreBuild.Deno)
- cfg.PreBuild.Npm = expandEnv(cfg.PreBuild.Npm)
-
- cfg.Build.Flags = expandEnvSlice(cfg.Build.Flags)
- cfg.Build.LDFlags = expandEnvSlice(cfg.Build.LDFlags)
- cfg.Build.BuildTags = expandEnvSlice(cfg.Build.BuildTags)
- cfg.Build.Env = expandEnvSlice(cfg.Build.Env)
- cfg.Build.Tags = expandEnvSlice(cfg.Build.Tags)
- cfg.Build.Formats = normalizeLinuxKitFormats(expandEnvSlice(cfg.Build.Formats))
- cfg.PreBuild = PreBuild{
- Deno: cfg.Build.DenoBuild,
- Npm: cfg.Build.NpmBuild,
- }
- cfg.LinuxKit.Base = expandEnv(cfg.LinuxKit.Base)
- cfg.LinuxKit.Packages = expandEnvSlice(cfg.LinuxKit.Packages)
- cfg.LinuxKit.Mounts = expandEnvSlice(cfg.LinuxKit.Mounts)
- cfg.LinuxKit.Formats = expandEnvSlice(cfg.LinuxKit.Formats)
- cfg.LinuxKit.Registry = expandEnv(cfg.LinuxKit.Registry)
- cfg.LinuxKit = normalizeLinuxKitConfig(cfg.LinuxKit)
- cfg.Apple.XcodeCloud.Triggers = expandXcodeCloudTriggers(cfg.Apple.XcodeCloud.Triggers)
-
- cfg.Build.Cache.Dir = expandEnv(cfg.Build.Cache.Dir)
- cfg.Build.Cache.Directory = cfg.Build.Cache.Dir
- cfg.Build.Cache.KeyPrefix = expandEnv(cfg.Build.Cache.KeyPrefix)
- cfg.Build.Cache.Paths = expandEnvSlice(cfg.Build.Cache.Paths)
- cfg.Build.Cache.RestoreKeys = expandEnvSlice(cfg.Build.Cache.RestoreKeys)
-
- cfg.Build.BuildArgs = expandEnvMap(cfg.Build.BuildArgs)
- cfg.Targets = expandTargetConfigs(cfg.Targets)
- if cfg.SDK != nil {
- cfg.SDK.Spec = expandEnv(cfg.SDK.Spec)
- cfg.SDK.Languages = expandEnvSlice(cfg.SDK.Languages)
- cfg.SDK.Output = expandEnv(cfg.SDK.Output)
- cfg.SDK.Package.Name = expandEnv(cfg.SDK.Package.Name)
- cfg.SDK.Package.Version = expandEnv(cfg.SDK.Package.Version)
- cfg.SDK.Publish.Repo = expandEnv(cfg.SDK.Publish.Repo)
- cfg.SDK.Publish.Path = expandEnv(cfg.SDK.Publish.Path)
- }
-
- cfg.Sign.ExpandEnv()
-}
-
-func expandEnvSlice(values []string) []string {
- if len(values) == 0 {
- return values
- }
-
- result := make([]string, len(values))
- for i, value := range values {
- result[i] = expandEnv(value)
- }
- return result
-}
-
-func expandEnvMap(values map[string]string) map[string]string {
- if len(values) == 0 {
- return values
- }
-
- result := make(map[string]string, len(values))
- for key, value := range values {
- result[key] = expandEnv(value)
- }
- return result
-}
-
-func expandTargetConfigs(values []TargetConfig) []TargetConfig {
- if len(values) == 0 {
- return values
- }
-
- result := make([]TargetConfig, len(values))
- for i, value := range values {
- result[i] = TargetConfig{
- OS: expandEnv(value.OS),
- Arch: expandEnv(value.Arch),
- }
- }
- return result
-}
-
-func expandXcodeCloudTriggers(values []XcodeCloudTrigger) []XcodeCloudTrigger {
- if len(values) == 0 {
- return values
- }
-
- result := make([]XcodeCloudTrigger, len(values))
- for i, value := range values {
- result[i] = XcodeCloudTrigger{
- Branch: expandEnv(value.Branch),
- Tag: expandEnv(value.Tag),
- Action: expandEnv(value.Action),
- }
- }
- return result
-}
-
-// CloneStringMap returns a shallow copy of a string map.
-//
-// clone := build.CloneStringMap(map[string]string{"VERSION": "v1.2.3"})
-func CloneStringMap(values map[string]string) map[string]string {
- if len(values) == 0 {
- return values
- }
-
- result := make(map[string]string, len(values))
- for key, value := range values {
- result[key] = value
- }
- return result
-}
-
-// CloneBuildConfig returns a deep copy of a build config so callers can apply
-// runtime overrides without mutating the persisted or caller-owned config.
-//
-// clone := build.CloneBuildConfig(cfg)
-func CloneBuildConfig(cfg *BuildConfig) *BuildConfig {
- if cfg == nil {
- return nil
- }
-
- clone := *cfg
- clone.Build = cloneBuild(cfg.Build)
- clone.Apple = cloneAppleConfig(cfg.Apple)
- clone.SDK = sdk.CloneConfig(cfg.SDK)
- clone.LinuxKit = cloneLinuxKitConfig(cfg.LinuxKit)
- clone.Targets = append([]TargetConfig(nil), cfg.Targets...)
-
- return &clone
-}
-
-func cloneBuild(value Build) Build {
- return Build{
- Type: value.Type,
- CGO: value.CGO,
- Obfuscate: value.Obfuscate,
- DenoBuild: value.DenoBuild,
- NpmBuild: value.NpmBuild,
- NSIS: value.NSIS,
- WebView2: value.WebView2,
- Flags: append([]string(nil), value.Flags...),
- LDFlags: append([]string(nil), value.LDFlags...),
- BuildTags: append([]string(nil), value.BuildTags...),
- ArchiveFormat: value.ArchiveFormat,
- Env: append([]string(nil), value.Env...),
- Cache: cloneCacheConfig(value.Cache),
- Dockerfile: value.Dockerfile,
- Registry: value.Registry,
- Image: value.Image,
- Tags: append([]string(nil), value.Tags...),
- BuildArgs: CloneStringMap(value.BuildArgs),
- Push: value.Push,
- Load: value.Load,
- LinuxKitConfig: value.LinuxKitConfig,
- Formats: append([]string(nil), value.Formats...),
- }
-}
-
-func cloneCacheConfig(value CacheConfig) CacheConfig {
- directory := value.effectiveDirectory()
- return CacheConfig{
- Enabled: value.Enabled,
- Dir: directory,
- Directory: directory,
- KeyPrefix: value.KeyPrefix,
- Paths: append([]string(nil), value.Paths...),
- RestoreKeys: append([]string(nil), value.RestoreKeys...),
- }
-}
-
-func cloneLinuxKitConfig(value LinuxKitConfig) LinuxKitConfig {
- return LinuxKitConfig{
- Base: value.Base,
- Packages: append([]string(nil), value.Packages...),
- Mounts: append([]string(nil), value.Mounts...),
- GPU: value.GPU,
- Formats: append([]string(nil), value.Formats...),
- Registry: value.Registry,
- }
-}
-
-func cloneAppleConfig(value AppleConfig) AppleConfig {
- clone := value
-
- if value.Sign != nil {
- sign := *value.Sign
- clone.Sign = &sign
- }
- if value.Notarise != nil {
- notarise := *value.Notarise
- clone.Notarise = ¬arise
- }
- if value.DMG != nil {
- dmg := *value.DMG
- clone.DMG = &dmg
- }
- if value.TestFlight != nil {
- testFlight := *value.TestFlight
- clone.TestFlight = &testFlight
- }
- if value.AppStore != nil {
- appStore := *value.AppStore
- clone.AppStore = &appStore
- }
-
- clone.XcodeCloud = XcodeCloudConfig{
- Workflow: value.XcodeCloud.Workflow,
- Triggers: append([]XcodeCloudTrigger(nil), value.XcodeCloud.Triggers...),
- }
-
- return clone
-}
-
-// expandEnv expands $VAR or ${VAR} using the current process environment.
-func expandEnv(s string) string {
- if !core.Contains(s, "$") {
- return s
- }
-
- buf := core.NewBuilder()
- for i := 0; i < len(s); {
- if s[i] != '$' {
- buf.WriteByte(s[i])
- i++
- continue
- }
-
- if i+1 < len(s) && s[i+1] == '{' {
- j := i + 2
- for j < len(s) && s[j] != '}' {
- j++
- }
- if j < len(s) {
- buf.WriteString(core.Env(s[i+2 : j]))
- i = j + 1
- continue
- }
- }
-
- j := i + 1
- for j < len(s) {
- c := s[j]
- if c != '_' && (c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') {
- break
- }
- j++
- }
- if j > i+1 {
- buf.WriteString(core.Env(s[i+1 : j]))
- i = j
- continue
- }
-
- buf.WriteByte(s[i])
- i++
- }
-
- return buf.String()
-}
-
-// ConfigPath returns the path to the build config file for a given directory.
-//
-// path := build.ConfigPath("/home/user/my-project") // → "/home/user/my-project/.core/build.yaml"
-func ConfigPath(dir string) string {
- return ax.Join(dir, ConfigDir, ConfigFileName)
-}
-
-// ConfigExists checks if a build config file exists in the given directory.
-//
-// if build.ConfigExists(storage.Local, ".") { ... }
-func ConfigExists(fs storage.Medium, dir string) bool {
- if fs == nil {
- return false
- }
- return fileExists(fs, ConfigPath(dir))
-}
-
-// TargetsIter returns an iterator for the build targets.
-//
-// for t := range cfg.TargetsIter() { fmt.Println(t.OS, t.Arch) }
-func (cfg *BuildConfig) TargetsIter() iter.Seq[TargetConfig] {
- return func(yield func(TargetConfig) bool) {
- for _, t := range cfg.Targets {
- if !yield(t) {
- return
- }
- }
- }
-}
-
-// ToTargets converts TargetConfig slice to Target slice for use with builders.
-//
-// targets := cfg.ToTargets()
-func (cfg *BuildConfig) ToTargets() []Target {
- targets := make([]Target, len(cfg.Targets))
- for i, t := range cfg.Targets {
- targets[i] = Target{OS: t.OS, Arch: t.Arch}
- }
- return targets
-}
diff --git a/pkg/build/config_example_test.go b/pkg/build/config_example_test.go
deleted file mode 100644
index 060b2e4..0000000
--- a/pkg/build/config_example_test.go
+++ /dev/null
@@ -1,122 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleTargetConfig_MarshalYAML references TargetConfig.MarshalYAML on this package API surface.
-func ExampleTargetConfig_MarshalYAML() {
- _ = (*TargetConfig).MarshalYAML
- core.Println("TargetConfig.MarshalYAML")
- // Output: TargetConfig.MarshalYAML
-}
-
-// ExampleTargetConfig_UnmarshalYAML references TargetConfig.UnmarshalYAML on this package API surface.
-func ExampleTargetConfig_UnmarshalYAML() {
- _ = (*TargetConfig).UnmarshalYAML
- core.Println("TargetConfig.UnmarshalYAML")
- // Output: TargetConfig.UnmarshalYAML
-}
-
-// ExampleBuildConfig_UnmarshalYAML references BuildConfig.UnmarshalYAML on this package API surface.
-func ExampleBuildConfig_UnmarshalYAML() {
- _ = (*BuildConfig).UnmarshalYAML
- core.Println("BuildConfig.UnmarshalYAML")
- // Output: BuildConfig.UnmarshalYAML
-}
-
-// ExampleBuildConfig_MarshalYAML references BuildConfig.MarshalYAML on this package API surface.
-func ExampleBuildConfig_MarshalYAML() {
- _ = (*BuildConfig).MarshalYAML
- core.Println("BuildConfig.MarshalYAML")
- // Output: BuildConfig.MarshalYAML
-}
-
-// ExampleLoadConfig references LoadConfig on this package API surface.
-func ExampleLoadConfig() {
- _ = LoadConfig
- core.Println("LoadConfig")
- // Output: LoadConfig
-}
-
-// ExampleLoadConfigAtPath references LoadConfigAtPath on this package API surface.
-func ExampleLoadConfigAtPath() {
- _ = LoadConfigAtPath
- core.Println("LoadConfigAtPath")
- // Output: LoadConfigAtPath
-}
-
-// ExampleDefaultConfig references DefaultConfig on this package API surface.
-func ExampleDefaultConfig() {
- _ = DefaultConfig
- core.Println("DefaultConfig")
- // Output: DefaultConfig
-}
-
-// ExampleResolveOutputMedium references ResolveOutputMedium on this package API surface.
-func ExampleResolveOutputMedium() {
- _ = ResolveOutputMedium
- core.Println("ResolveOutputMedium")
- // Output: ResolveOutputMedium
-}
-
-// ExampleMediumIsLocal references MediumIsLocal on this package API surface.
-func ExampleMediumIsLocal() {
- _ = MediumIsLocal
- core.Println("MediumIsLocal")
- // Output: MediumIsLocal
-}
-
-// ExampleCopyMediumPath references CopyMediumPath on this package API surface.
-func ExampleCopyMediumPath() {
- _ = CopyMediumPath
- core.Println("CopyMediumPath")
- // Output: CopyMediumPath
-}
-
-// ExampleBuildConfig_ExpandEnv references BuildConfig.ExpandEnv on this package API surface.
-func ExampleBuildConfig_ExpandEnv() {
- _ = (*BuildConfig).ExpandEnv
- core.Println("BuildConfig.ExpandEnv")
- // Output: BuildConfig.ExpandEnv
-}
-
-// ExampleCloneStringMap references CloneStringMap on this package API surface.
-func ExampleCloneStringMap() {
- _ = CloneStringMap
- core.Println("CloneStringMap")
- // Output: CloneStringMap
-}
-
-// ExampleCloneBuildConfig references CloneBuildConfig on this package API surface.
-func ExampleCloneBuildConfig() {
- _ = CloneBuildConfig
- core.Println("CloneBuildConfig")
- // Output: CloneBuildConfig
-}
-
-// ExampleConfigPath references ConfigPath on this package API surface.
-func ExampleConfigPath() {
- _ = ConfigPath
- core.Println("ConfigPath")
- // Output: ConfigPath
-}
-
-// ExampleConfigExists references ConfigExists on this package API surface.
-func ExampleConfigExists() {
- _ = ConfigExists
- core.Println("ConfigExists")
- // Output: ConfigExists
-}
-
-// ExampleBuildConfig_TargetsIter references BuildConfig.TargetsIter on this package API surface.
-func ExampleBuildConfig_TargetsIter() {
- _ = (*BuildConfig).TargetsIter
- core.Println("BuildConfig.TargetsIter")
- // Output: BuildConfig.TargetsIter
-}
-
-// ExampleBuildConfig_ToTargets references BuildConfig.ToTargets on this package API surface.
-func ExampleBuildConfig_ToTargets() {
- _ = (*BuildConfig).ToTargets
- core.Println("BuildConfig.ToTargets")
- // Output: BuildConfig.ToTargets
-}
diff --git a/pkg/build/config_test.go b/pkg/build/config_test.go
deleted file mode 100644
index c2d53f4..0000000
--- a/pkg/build/config_test.go
+++ /dev/null
@@ -1,1885 +0,0 @@
-package build
-
-import (
- "testing"
-
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/testassert"
- "dappco.re/go/build/pkg/sdk"
- storage "dappco.re/go/build/pkg/storage"
-
- core "dappco.re/go"
- "gopkg.in/yaml.v3"
-)
-
-// setupConfigTestDir creates a temp directory with optional .core/build.yaml content.
-func setupConfigTestDir(t *testing.T, configContent string) string {
- t.Helper()
- dir := t.TempDir()
-
- if configContent != "" {
- coreDir := ax.Join(dir, ConfigDir)
- requireConfigOKResult(t, ax.MkdirAll(coreDir, 0755))
-
- configPath := ax.Join(coreDir, ConfigFileName)
- requireConfigOKResult(t, ax.WriteFile(configPath, []byte(configContent), 0644))
-
- }
-
- return dir
-}
-
-func requireConfigOKResult(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requireConfigOK(t *testing.T, result core.Result) *BuildConfig {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(*BuildConfig)
-}
-
-func requireConfigError(t *testing.T, result core.Result) string {
- t.Helper()
- if result.OK {
- t.Fatal("expected error")
- }
- return result.Error()
-}
-
-func requireConfigBytes(t *testing.T, result core.Result) []byte {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]byte)
-}
-
-func requireConfigMap(t *testing.T, result core.Result) map[string]string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(map[string]string)
-}
-
-func requireConfigBuildYAML(t *testing.T, result core.Result) buildConfigYAML {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(buildConfigYAML)
-}
-
-func requireConfigString(t *testing.T, result core.Result) string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func TestConfig_LoadConfig_Good(t *testing.T) {
- fs := storage.Local
- t.Run("loads valid config", func(t *testing.T) {
- content := `
-version: 1
-project:
- name: myapp
- description: A test application
- main: ./cmd/myapp
- binary: myapp
-build:
- cgo: true
- flags:
- - -trimpath
- - -race
- ldflags:
- - -s
- - -w
- build_tags:
- - integration
- - webkit2_41
- archive_format: xz
- env:
- - FOO=bar
- load: true
-targets:
- - os: linux
- arch: amd64
- - os: darwin
- arch: arm64
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(1, cfg.Version) {
- t.Fatalf("want %v, got %v", 1, cfg.Version)
- }
- if !stdlibAssertEqual("myapp", cfg.Project.Name) {
- t.Fatalf("want %v, got %v", "myapp", cfg.Project.Name)
- }
- if !stdlibAssertEqual("A test application", cfg.Project.Description) {
- t.Fatalf("want %v, got %v", "A test application", cfg.Project.Description)
- }
- if !stdlibAssertEqual("./cmd/myapp", cfg.Project.Main) {
- t.Fatalf("want %v, got %v", "./cmd/myapp", cfg.Project.Main)
- }
- if !stdlibAssertEqual("myapp", cfg.Project.Binary) {
- t.Fatalf("want %v, got %v", "myapp", cfg.Project.Binary)
- }
- if !(cfg.Build.CGO) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual([]string{"-trimpath", "-race"}, cfg.Build.Flags) {
- t.Fatalf("want %v, got %v", []string{"-trimpath", "-race"}, cfg.Build.Flags)
- }
- if !stdlibAssertEqual([]string{"-s", "-w"}, cfg.Build.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w"}, cfg.Build.LDFlags)
- }
- if !stdlibAssertEqual([]string{"integration", "webkit2_41"}, cfg.Build.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"integration", "webkit2_41"}, cfg.Build.BuildTags)
- }
- if !stdlibAssertEqual("xz", cfg.Build.ArchiveFormat) {
- t.Fatalf("want %v, got %v", "xz", cfg.Build.ArchiveFormat)
- }
- if !stdlibAssertEqual([]string{"FOO=bar"}, cfg.Build.Env) {
- t.Fatalf("want %v, got %v", []string{"FOO=bar"}, cfg.Build.Env)
- }
- if !(cfg.Build.Load) {
- t.Fatal("expected true")
- }
- if len(cfg.Targets) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(cfg.Targets))
- }
- if !stdlibAssertEqual("linux", cfg.Targets[0].OS) {
- t.Fatalf("want %v, got %v", "linux", cfg.Targets[0].OS)
- }
- if !stdlibAssertEqual("amd64", cfg.Targets[0].Arch) {
- t.Fatalf("want %v, got %v", "amd64", cfg.Targets[0].Arch)
- }
- if !stdlibAssertEqual("darwin", cfg.Targets[1].OS) {
- t.Fatalf("want %v, got %v", "darwin", cfg.Targets[1].OS)
- }
- if !stdlibAssertEqual("arm64", cfg.Targets[1].Arch) {
- t.Fatalf("want %v, got %v", "arm64", cfg.Targets[1].Arch)
- }
-
- })
-
- t.Run("defaults to the local medium when nil is passed", func(t *testing.T) {
- content := `
-version: 1
-project:
- name: nil-medium
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(nil, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("nil-medium", cfg.Project.Name) {
- t.Fatalf("want %v, got %v", "nil-medium", cfg.Project.Name)
- }
-
- })
-
- t.Run("expands environment variables in target config", func(t *testing.T) {
- t.Setenv("TARGET_OS", "linux")
- t.Setenv("TARGET_ARCH", "arm64")
-
- content := `
-version: 1
-targets:
- - os: ${TARGET_OS}
- arch: ${TARGET_ARCH}
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if len(cfg.Targets) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(cfg.Targets))
- }
- if !stdlibAssertEqual("linux", cfg.Targets[0].OS) {
- t.Fatalf("want %v, got %v", "linux", cfg.Targets[0].OS)
- }
- if !stdlibAssertEqual("arm64", cfg.Targets[0].Arch) {
- t.Fatalf("want %v, got %v", "arm64", cfg.Targets[0].Arch)
- }
-
- })
-
- t.Run("expands environment variables in build and signing config", func(t *testing.T) {
- t.Setenv("APP_NAME", "demo-app")
- t.Setenv("APP_ROOT", "./cmd/demo")
- t.Setenv("APP_BINARY", "demo-bin")
- t.Setenv("BUILD_TYPE", "wails")
- t.Setenv("DENO_BUILD", "deno task bundle")
- t.Setenv("WEBVIEW2", "embed")
- t.Setenv("ARCHIVE_FORMAT", "xz")
- t.Setenv("APP_VERSION", "v1.2.3")
- t.Setenv("APP_TAG", "integration")
- t.Setenv("CACHE_DIR", ".core/cache/demo-app")
- t.Setenv("DOCKERFILE", "Dockerfile.release")
- t.Setenv("IMAGE_NAME", "owner/demo-app")
- t.Setenv("GPG_KEY_ID", "ABCD1234")
-
- content := `
-version: 1
-project:
- name: ${APP_NAME}
- main: ${APP_ROOT}
- binary: ${APP_BINARY}
-build:
- type: ${BUILD_TYPE}
- deno_build: ${DENO_BUILD}
- webview2: ${WEBVIEW2}
- archive_format: ${ARCHIVE_FORMAT}
- flags:
- - -trimpath
- - -X
- - main.version=${APP_VERSION}
- ldflags:
- - -s
- - -w
- build_tags:
- - ${APP_TAG}
- env:
- - VERSION=${APP_VERSION}
- cache:
- enabled: true
- dir: ${CACHE_DIR}
- paths:
- - ${CACHE_DIR}/go-build
- dockerfile: ${DOCKERFILE}
- image: ${IMAGE_NAME}
- tags:
- - latest
- - ${APP_VERSION}
- build_args:
- VERSION: ${APP_VERSION}
-sign:
- gpg:
- key: ${GPG_KEY_ID}
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("demo-app", cfg.Project.Name) {
- t.Fatalf("want %v, got %v", "demo-app", cfg.Project.Name)
- }
- if !stdlibAssertEqual("./cmd/demo", cfg.Project.Main) {
- t.Fatalf("want %v, got %v", "./cmd/demo", cfg.Project.Main)
- }
- if !stdlibAssertEqual("demo-bin", cfg.Project.Binary) {
- t.Fatalf("want %v, got %v", "demo-bin", cfg.Project.Binary)
- }
- if !stdlibAssertEqual("wails", cfg.Build.Type) {
- t.Fatalf("want %v, got %v", "wails", cfg.Build.Type)
- }
- if !stdlibAssertEqual("deno task bundle", cfg.Build.DenoBuild) {
- t.Fatalf("want %v, got %v", "deno task bundle", cfg.Build.DenoBuild)
- }
- if !stdlibAssertEqual("embed", cfg.Build.WebView2) {
- t.Fatalf("want %v, got %v", "embed", cfg.Build.WebView2)
- }
- if !stdlibAssertEqual("xz", cfg.Build.ArchiveFormat) {
- t.Fatalf("want %v, got %v", "xz", cfg.Build.ArchiveFormat)
- }
- if !stdlibAssertEqual([]string{"-trimpath", "-X", "main.version=v1.2.3"}, cfg.Build.Flags) {
- t.Fatalf("want %v, got %v", []string{"-trimpath", "-X", "main.version=v1.2.3"}, cfg.Build.Flags)
- }
- if !stdlibAssertEqual([]string{"-s", "-w"}, cfg.Build.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w"}, cfg.Build.LDFlags)
- }
- if !stdlibAssertEqual([]string{"integration"}, cfg.Build.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"integration"}, cfg.Build.BuildTags)
- }
- if !stdlibAssertEqual([]string{"VERSION=v1.2.3"}, cfg.Build.Env) {
- t.Fatalf("want %v, got %v", []string{"VERSION=v1.2.3"}, cfg.Build.Env)
- }
- if !stdlibAssertEqual(".core/cache/demo-app", cfg.Build.Cache.Directory) {
- t.Fatalf("want %v, got %v", ".core/cache/demo-app", cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEqual([]string{".core/cache/demo-app/go-build"}, cfg.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{".core/cache/demo-app/go-build"}, cfg.Build.Cache.Paths)
- }
- if !stdlibAssertEqual("Dockerfile.release", cfg.Build.Dockerfile) {
- t.Fatalf("want %v, got %v", "Dockerfile.release", cfg.Build.Dockerfile)
- }
- if !stdlibAssertEqual("owner/demo-app", cfg.Build.Image) {
- t.Fatalf("want %v, got %v", "owner/demo-app", cfg.Build.Image)
- }
- if !stdlibAssertEqual([]string{"latest", "v1.2.3"}, cfg.Build.Tags) {
- t.Fatalf("want %v, got %v", []string{"latest", "v1.2.3"}, cfg.Build.Tags)
- }
- if !stdlibAssertEqual(map[string]string{"VERSION": "v1.2.3"}, cfg.Build.BuildArgs) {
- t.Fatalf("want %v, got %v", map[string]string{"VERSION": "v1.2.3"}, cfg.Build.BuildArgs)
- }
- if !stdlibAssertEqual("ABCD1234", cfg.Sign.GPG.Key) {
- t.Fatalf("want %v, got %v", "ABCD1234", cfg.Sign.GPG.Key)
- }
-
- })
-
- t.Run("loads RFC build flags for obfuscation and NSIS", func(t *testing.T) {
- content := `
-version: 1
-build:
- obfuscate: true
- nsis: true
- webview2: download
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !(cfg.Build.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(cfg.Build.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("download", cfg.Build.WebView2) {
- t.Fatalf("want %v, got %v", "download", cfg.Build.WebView2)
- }
-
- })
-
- t.Run("supports top-level cache block from the RFC", func(t *testing.T) {
- content := `
-version: 1
-cache:
- enabled: true
- dir: .core/cache
- paths:
- - ~/.cache/go-build
- - ~/go/pkg/mod
- restore_keys:
- - go-
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !(cfg.Build.Cache.Enabled) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(".core/cache", cfg.Build.Cache.Directory) {
- t.Fatalf("want %v, got %v", ".core/cache", cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEqual([]string{"~/.cache/go-build", "~/go/pkg/mod"}, cfg.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{"~/.cache/go-build", "~/go/pkg/mod"}, cfg.Build.Cache.Paths)
- }
- if !stdlibAssertEqual([]string{"go-"}, cfg.Build.Cache.RestoreKeys) {
- t.Fatalf("want %v, got %v", []string{"go-"}, cfg.Build.Cache.RestoreKeys)
- }
-
- })
-
- t.Run("supports RFC pre_build block for frontend hooks", func(t *testing.T) {
- t.Setenv("DENO_BUILD", "deno task bundle")
- t.Setenv("NPM_BUILD", "npm run bundle")
-
- content := `
-version: 1
-pre_build:
- deno: ${DENO_BUILD}
- npm: ${NPM_BUILD}
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("deno task bundle", cfg.Build.DenoBuild) {
- t.Fatalf("want %v, got %v", "deno task bundle", cfg.Build.DenoBuild)
- }
- if !stdlibAssertEqual("npm run bundle", cfg.Build.NpmBuild) {
- t.Fatalf("want %v, got %v", "npm run bundle", cfg.Build.NpmBuild)
- }
- if !stdlibAssertEqual(PreBuild{Deno: "deno task bundle", Npm: "npm run bundle"}, cfg.PreBuild) {
- t.Fatalf("want %v, got %v", PreBuild{Deno: "deno task bundle", Npm: "npm run bundle"}, cfg.PreBuild)
- }
-
- })
-
- t.Run("keeps legacy build frontend hooks when both shapes are present", func(t *testing.T) {
- content := `
-version: 1
-build:
- deno_build: deno task legacy
- npm_build: npm run legacy
-pre_build:
- deno: deno task ignored
- npm: npm run ignored
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("deno task legacy", cfg.Build.DenoBuild) {
- t.Fatalf("want %v, got %v", "deno task legacy", cfg.Build.DenoBuild)
- }
- if !stdlibAssertEqual("npm run legacy", cfg.Build.NpmBuild) {
- t.Fatalf("want %v, got %v", "npm run legacy", cfg.Build.NpmBuild)
- }
- if !stdlibAssertEqual(PreBuild{Deno: "deno task legacy", Npm: "npm run legacy"}, cfg.PreBuild) {
- t.Fatalf("want %v, got %v", PreBuild{Deno: "deno task legacy", Npm: "npm run legacy"}, cfg.PreBuild)
- }
-
- })
-
- t.Run("loads apple pipeline config with env expansion", func(t *testing.T) {
- t.Setenv("APPLE_TEAM_ID", "ABC123DEF4")
- t.Setenv("APPLE_BUNDLE_ID", "ai.lthn.core")
- t.Setenv("APPLE_CERT_ID", "Developer ID Application: Lethean CIC (ABC123DEF4)")
- t.Setenv("APPLE_KEY_PATH", "/tmp/AuthKey_TEST.p8")
- t.Setenv("APPLE_METADATA_PATH", ".core/apple/appstore")
- t.Setenv("APPLE_PRIVACY_URL", "https://lthn.ai/privacy")
- t.Setenv("APPLE_BG", "assets/dmg-background.png")
- t.Setenv("XCLOUD_WORKFLOW", "CoreGUI Release")
- t.Setenv("XCLOUD_BRANCH", "main")
-
- content := `
-version: 1
-apple:
- team_id: ${APPLE_TEAM_ID}
- bundle_id: ${APPLE_BUNDLE_ID}
- arch: universal
- cert_identity: ${APPLE_CERT_ID}
- sign: false
- notarise: true
- dmg: true
- metadata_path: ${APPLE_METADATA_PATH}
- privacy_policy_url: ${APPLE_PRIVACY_URL}
- api_key_path: ${APPLE_KEY_PATH}
- dmg_background: ${APPLE_BG}
- xcode_cloud:
- workflow: ${XCLOUD_WORKFLOW}
- triggers:
- - branch: ${XCLOUD_BRANCH}
- action: testflight
- - tag: v*
- action: appstore
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("ABC123DEF4", cfg.Apple.TeamID) {
- t.Fatalf("want %v, got %v", "ABC123DEF4", cfg.Apple.TeamID)
- }
- if !stdlibAssertEqual("ai.lthn.core", cfg.Apple.BundleID) {
- t.Fatalf("want %v, got %v", "ai.lthn.core", cfg.Apple.BundleID)
- }
- if !stdlibAssertEqual("universal", cfg.Apple.Arch) {
- t.Fatalf("want %v, got %v", "universal", cfg.Apple.Arch)
- }
- if !stdlibAssertEqual("Developer ID Application: Lethean CIC (ABC123DEF4)", cfg.Apple.CertIdentity) {
- t.Fatalf("want %v, got %v", "Developer ID Application: Lethean CIC (ABC123DEF4)", cfg.Apple.CertIdentity)
- }
- if stdlibAssertNil(cfg.Apple.Sign) {
- t.Fatal("expected non-nil")
- }
- if *cfg.Apple.Sign {
- t.Fatal("expected false")
- }
- if stdlibAssertNil(cfg.Apple.Notarise) {
- t.Fatal("expected non-nil")
- }
- if !(*cfg.Apple.Notarise) {
- t.Fatal("expected true")
- }
- if stdlibAssertNil(cfg.Apple.DMG) {
- t.Fatal("expected non-nil")
- }
- if !(*cfg.Apple.DMG) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(".core/apple/appstore", cfg.Apple.MetadataPath) {
- t.Fatalf("want %v, got %v", ".core/apple/appstore", cfg.Apple.MetadataPath)
- }
- if !stdlibAssertEqual("https://lthn.ai/privacy", cfg.Apple.PrivacyPolicyURL) {
- t.Fatalf("want %v, got %v", "https://lthn.ai/privacy", cfg.Apple.PrivacyPolicyURL)
- }
- if !stdlibAssertEqual("/tmp/AuthKey_TEST.p8", cfg.Apple.APIKeyPath) {
- t.Fatalf("want %v, got %v", "/tmp/AuthKey_TEST.p8", cfg.Apple.APIKeyPath)
- }
- if !stdlibAssertEqual("assets/dmg-background.png", cfg.Apple.DMGBackground) {
- t.Fatalf("want %v, got %v", "assets/dmg-background.png", cfg.Apple.DMGBackground)
- }
- if !stdlibAssertEqual("CoreGUI Release", cfg.Apple.XcodeCloud.Workflow) {
- t.Fatalf("want %v, got %v", "CoreGUI Release", cfg.Apple.XcodeCloud.Workflow)
- }
- if len(cfg.Apple.XcodeCloud.Triggers) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(cfg.Apple.XcodeCloud.Triggers))
- }
- if !stdlibAssertEqual("main", cfg.Apple.XcodeCloud.Triggers[0].Branch) {
- t.Fatalf("want %v, got %v", "main", cfg.Apple.XcodeCloud.Triggers[0].Branch)
- }
- if !stdlibAssertEqual("testflight", cfg.Apple.XcodeCloud.Triggers[0].Action) {
- t.Fatalf("want %v, got %v", "testflight", cfg.Apple.XcodeCloud.Triggers[0].Action)
- }
- if !stdlibAssertEqual("v*", cfg.Apple.XcodeCloud.Triggers[1].Tag) {
- t.Fatalf("want %v, got %v", "v*", cfg.Apple.XcodeCloud.Triggers[1].Tag)
- }
- if !stdlibAssertEqual("appstore", cfg.Apple.XcodeCloud.Triggers[1].Action) {
- t.Fatalf("want %v, got %v", "appstore", cfg.Apple.XcodeCloud.Triggers[1].Action)
- }
-
- })
-
- t.Run("loads immutable LinuxKit image config with env expansion", func(t *testing.T) {
- t.Setenv("CORE_IMAGE_BASE", "core-ml")
- t.Setenv("CORE_IMAGE_PACKAGE", "gh")
- t.Setenv("CORE_IMAGE_MOUNT", "/workspace")
- t.Setenv("CORE_IMAGE_FORMAT", "oci")
- t.Setenv("CORE_IMAGE_REGISTRY", "ghcr.io/dappcore")
-
- content := `
-version: 1
-linuxkit:
- base: ${CORE_IMAGE_BASE}
- packages:
- - ${CORE_IMAGE_PACKAGE}
- mounts:
- - ${CORE_IMAGE_MOUNT}
- gpu: true
- formats:
- - ${CORE_IMAGE_FORMAT}
- - apple
- registry: ${CORE_IMAGE_REGISTRY}
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("core-ml", cfg.LinuxKit.Base) {
- t.Fatalf("want %v, got %v", "core-ml", cfg.LinuxKit.Base)
- }
- if !stdlibAssertEqual([]string{"gh"}, cfg.LinuxKit.Packages) {
- t.Fatalf("want %v, got %v", []string{"gh"}, cfg.LinuxKit.Packages)
- }
- if !stdlibAssertEqual([]string{"/workspace"}, cfg.LinuxKit.Mounts) {
- t.Fatalf("want %v, got %v", []string{"/workspace"}, cfg.LinuxKit.Mounts)
- }
- if !(cfg.LinuxKit.GPU) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual([]string{"oci", "apple"}, cfg.LinuxKit.Formats) {
- t.Fatalf("want %v, got %v", []string{"oci", "apple"}, cfg.LinuxKit.Formats)
- }
- if !stdlibAssertEqual("ghcr.io/dappcore", cfg.LinuxKit.Registry) {
- t.Fatalf("want %v, got %v", "ghcr.io/dappcore", cfg.LinuxKit.Registry)
- }
-
- })
-
- t.Run("normalizes LinuxKit list values and formats", func(t *testing.T) {
- content := `
-version: 1
-build:
- formats:
- - " OCI "
- - apple
- - APPLE
-linuxkit:
- base: " core-dev "
- packages:
- - " git "
- - git
- - task
- mounts:
- - " /workspace "
- - /workspace
- - /src
- formats:
- - " OCI "
- - apple
- - APPLE
- registry: " ghcr.io/dappcore "
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual([]string{"oci", "apple"}, cfg.Build.Formats) {
- t.Fatalf("want %v, got %v", []string{"oci", "apple"}, cfg.Build.Formats)
- }
- if !stdlibAssertEqual("core-dev", cfg.LinuxKit.Base) {
- t.Fatalf("want %v, got %v", "core-dev", cfg.LinuxKit.Base)
- }
- if !stdlibAssertEqual([]string{"git", "task"}, cfg.LinuxKit.Packages) {
- t.Fatalf("want %v, got %v", []string{"git", "task"}, cfg.LinuxKit.Packages)
- }
- if !stdlibAssertEqual([]string{"/workspace", "/src"}, cfg.LinuxKit.Mounts) {
- t.Fatalf("want %v, got %v", []string{"/workspace", "/src"}, cfg.LinuxKit.Mounts)
- }
- if !stdlibAssertEqual([]string{"oci", "apple"}, cfg.LinuxKit.Formats) {
- t.Fatalf("want %v, got %v", []string{"oci", "apple"}, cfg.LinuxKit.Formats)
- }
- if !stdlibAssertEqual("ghcr.io/dappcore", cfg.LinuxKit.Registry) {
- t.Fatalf("want %v, got %v", "ghcr.io/dappcore", cfg.LinuxKit.Registry)
- }
-
- })
-
- t.Run("restores default LinuxKit base mounts and formats when expansion resolves empty", func(t *testing.T) {
- t.Setenv("CORE_IMAGE_BASE", "")
- t.Setenv("CORE_IMAGE_MOUNT", "")
- t.Setenv("CORE_IMAGE_FORMAT", "")
-
- content := `
-version: 1
-linuxkit:
- base: ${CORE_IMAGE_BASE}
- mounts:
- - ${CORE_IMAGE_MOUNT}
- formats:
- - ${CORE_IMAGE_FORMAT}
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("core-dev", cfg.LinuxKit.Base) {
- t.Fatalf("want %v, got %v", "core-dev", cfg.LinuxKit.Base)
- }
- if !stdlibAssertEqual([]string{"/workspace"}, cfg.LinuxKit.Mounts) {
- t.Fatalf("want %v, got %v", []string{"/workspace"}, cfg.LinuxKit.Mounts)
- }
- if !stdlibAssertEqual([]string{"oci", "apple"}, cfg.LinuxKit.Formats) {
- t.Fatalf("want %v, got %v", []string{"oci", "apple"}, cfg.LinuxKit.Formats)
- }
-
- })
-
- t.Run("loads sdk config from build yaml with shorthand diff and defaults", func(t *testing.T) {
- t.Setenv("SDK_SPEC", "docs/openapi.yaml")
- t.Setenv("SDK_LANG", "typescript")
-
- content := `
-version: 1
-sdk:
- spec: ${SDK_SPEC}
- languages:
- - ${SDK_LANG}
- skip_unavailable: true
- diff: true
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if stdlibAssertNil(cfg.SDK) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("docs/openapi.yaml", cfg.SDK.Spec) {
- t.Fatalf("want %v, got %v", "docs/openapi.yaml", cfg.SDK.Spec)
- }
- if !stdlibAssertEqual([]string{"typescript"}, cfg.SDK.Languages) {
- t.Fatalf("want %v, got %v", []string{"typescript"}, cfg.SDK.Languages)
- }
- if !stdlibAssertEqual("sdk", cfg.SDK.Output) {
- t.Fatalf("want %v, got %v", "sdk", cfg.SDK.Output)
- }
- if !(cfg.SDK.SkipUnavailable) {
- t.Fatal("expected true")
- }
- if !(cfg.SDK.Diff.Enabled) {
- t.Fatal("expected true")
- }
- if cfg.SDK.Diff.FailOnBreaking {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("preserves explicit empty sdk languages list", func(t *testing.T) {
- content := `
-version: 1
-sdk:
- languages: []
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if stdlibAssertNil(cfg.SDK) {
- t.Fatal("expected non-nil")
- }
- if stdlibAssertNil(cfg.SDK.Languages) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEmpty(cfg.SDK.Languages) {
- t.Fatalf("expected empty, got %v", cfg.SDK.Languages)
- }
-
- })
-
- t.Run("honours explicit windows signtool disablement", func(t *testing.T) {
- content := `
-version: 1
-sign:
- windows:
- signtool: false
- certificate: C:/certs/core.pfx
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if cfg.Sign.Windows.Signtool {
- t.Fatal("expected false")
- }
- if !stdlibAssertEqual("C:/certs/core.pfx", cfg.Sign.Windows.Certificate) {
- t.Fatalf("want %v, got %v", "C:/certs/core.pfx", cfg.Sign.Windows.Certificate)
- }
-
- })
- t.Run("returns defaults when config file missing", func(t *testing.T) {
- dir := t.TempDir()
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
-
- defaults := DefaultConfig()
- if !stdlibAssertEqual(defaults.Version, cfg.Version) {
- t.Fatalf("want %v, got %v", defaults.Version, cfg.Version)
- }
- if !stdlibAssertEqual(defaults.Project.Main, cfg.Project.Main) {
- t.Fatalf("want %v, got %v", defaults.Project.Main, cfg.Project.Main)
- }
- if !stdlibAssertEqual(defaults.Build.CGO, cfg.Build.CGO) {
- t.Fatalf("want %v, got %v", defaults.Build.CGO, cfg.Build.CGO)
- }
- if !stdlibAssertEqual(defaults.Build.Flags, cfg.Build.Flags) {
- t.Fatalf("want %v, got %v", defaults.Build.Flags, cfg.Build.Flags)
- }
- if !stdlibAssertEqual(defaults.Build.LDFlags, cfg.Build.LDFlags) {
- t.Fatalf("want %v, got %v", defaults.Build.LDFlags, cfg.Build.LDFlags)
- }
- if cfg.Build.Load {
- t.Fatal("expected false")
- }
- if !stdlibAssertEmpty(
-
- // Explicit values preserved
- cfg.Build.BuildTags) {
- t.Fatalf("expected empty, got %v", cfg.Build.BuildTags)
- }
- if !stdlibAssertEqual(defaults.
-
- // Defaults applied
- Targets, cfg.Targets) {
- t.Fatalf("want %v, got %v", defaults.Targets, cfg.Targets)
- }
-
- })
-
- t.Run("applies defaults for missing fields", func(t *testing.T) {
- content := `
-version: 2
-project:
- name: partial
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(2, cfg.Version) {
- t.Fatalf("want %v, got %v", 2, cfg.Version)
- }
- if !stdlibAssertEqual("partial", cfg.Project.Name) {
- t.Fatalf("want %v, got %v", "partial", cfg.Project.Name)
- }
-
- defaults := DefaultConfig()
- if !stdlibAssertEqual(defaults.Project.Main, cfg.Project.Main) {
- t.Fatalf("want %v, got %v", defaults.Project.Main, cfg.Project.Main)
- }
- if !stdlibAssertEqual(defaults.Build.Flags, cfg.Build.Flags) {
- t.Fatalf("want %v, got %v", defaults.Build.Flags, cfg.Build.Flags)
- }
- if !stdlibAssertEqual(defaults.Build.LDFlags, cfg.Build.LDFlags) {
- t.Fatalf("want %v, got %v", defaults.Build.LDFlags, cfg.Build.LDFlags)
- }
- if !stdlibAssertEqual(defaults.Targets, cfg.Targets) {
- t.Fatalf("want %v, got %v", defaults.Targets, cfg.Targets)
- }
- if !(cfg.Sign.Enabled) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("preserves explicit signing disablement", func(t *testing.T) {
- content := `
-version: 1
-sign:
- enabled: false
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if cfg.Sign.Enabled {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("preserves empty arrays when explicitly set", func(t *testing.T) {
- content := `
-version: 1
-project:
- name: noflags
-build:
- flags: []
- ldflags: []
- build_tags: []
-targets:
- - os: linux
- arch: amd64
-`
- dir := setupConfigTestDir(t, content)
-
- cfg := requireConfigOK(t, LoadConfig(fs, dir))
- if stdlibAssertNil(
-
- // Empty arrays are preserved (not replaced with defaults)
- cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEmpty(cfg.Build.Flags) {
- t.Fatalf("expected empty, got %v", cfg.Build.Flags)
- }
- if !stdlibAssertEmpty(cfg.Build.LDFlags) {
-
- // Targets explicitly set
- t.Fatalf("expected empty, got %v", cfg.Build.LDFlags)
- }
- if !stdlibAssertEmpty(cfg.Build.BuildTags) {
- t.Fatalf("expected empty, got %v", cfg.Build.BuildTags)
- }
- if len(cfg.Targets) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(cfg.Targets))
- }
-
- })
-}
-
-func TestConfig_MarshalYAMLGood(t *testing.T) {
- t.Run("emits the RFC top-level cache block", func(t *testing.T) {
- cfg := DefaultConfig()
- cfg.Project.Name = "demo"
- cfg.Build.Cache = CacheConfig{
- Enabled: true,
- Directory: ".core/cache",
- KeyPrefix: "demo",
- Paths: []string{"cache/go-build", "cache/go-mod"},
- RestoreKeys: []string{"go-"},
- }
-
- decoded := requireConfigBuildYAML(t, cfg.MarshalYAML())
- if stdlibAssertNil(decoded.Cache) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(true, decoded.Cache.Enabled) {
- t.Fatalf("want %v, got %v", true, decoded.Cache.Enabled)
- }
- if !stdlibAssertEqual(".core/cache", decoded.Cache.Dir) {
- t.Fatalf("want %v, got %v", ".core/cache", decoded.Cache.Dir)
- }
- if !stdlibAssertEqual("demo", decoded.Cache.KeyPrefix) {
- t.Fatalf("want %v, got %v", "demo", decoded.Cache.KeyPrefix)
- }
-
- })
-
- t.Run("omits cache when it is not configured", func(t *testing.T) {
- cfg := DefaultConfig()
- cfg.Build.Cache = CacheConfig{}
-
- decoded := requireConfigBuildYAML(t, cfg.MarshalYAML())
- if !stdlibAssertNil(decoded.Cache) {
- t.Fatalf("expected nil, got %v", decoded.Cache)
- }
-
- })
-
- t.Run("emits the RFC pre_build block instead of legacy build hooks", func(t *testing.T) {
- cfg := DefaultConfig()
- cfg.Build.DenoBuild = "deno task build"
- cfg.Build.NpmBuild = "npm run build"
- cfg.PreBuild = PreBuild{
- Deno: "deno task build",
- Npm: "npm run build",
- }
-
- decoded := requireConfigBuildYAML(t, cfg.MarshalYAML())
- if stdlibAssertNil(decoded.PreBuild) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("deno task build", decoded.PreBuild.Deno) {
- t.Fatalf("want %v, got %v", "deno task build", decoded.PreBuild.Deno)
- }
- if !stdlibAssertEqual("npm run build", decoded.PreBuild.Npm) {
- t.Fatalf("want %v, got %v", "npm run build", decoded.PreBuild.Npm)
- }
- if decoded.Build.DenoBuild != "" {
- t.Fatal("expected false")
- }
- if decoded.Build.NpmBuild != "" {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestConfig_LoadConfigAtPath_Good(t *testing.T) {
- fs := storage.Local
-
- t.Run("loads config from explicit file path", func(t *testing.T) {
- dir := t.TempDir()
- configPath := ax.Join(dir, "custom-build.yaml")
- content := `
-version: 3
-project:
- name: custom-app
- binary: custom-app
-build:
- cgo: true
-targets:
- - os: linux
- arch: amd64
-`
- requireConfigOKResult(t, ax.WriteFile(configPath, []byte(content), 0644))
-
- cfg := requireConfigOK(t, LoadConfigAtPath(fs, configPath))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(3, cfg.Version) {
- t.Fatalf("want %v, got %v", 3, cfg.Version)
- }
- if !stdlibAssertEqual("custom-app", cfg.Project.Name) {
- t.Fatalf("want %v, got %v", "custom-app", cfg.Project.Name)
- }
- if !stdlibAssertEqual("custom-app", cfg.Project.Binary) {
- t.Fatalf("want %v, got %v", "custom-app", cfg.Project.Binary)
- }
- if !(cfg.Build.CGO) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEmpty(cfg.Build.BuildTags) {
- t.Fatalf("expected empty, got %v", cfg.Build.BuildTags)
- }
- if len(cfg.Targets) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(cfg.Targets))
- }
- if !stdlibAssertEqual("linux", cfg.Targets[0].OS) {
- t.Fatalf("want %v, got %v", "linux", cfg.Targets[0].OS)
- }
- if !stdlibAssertEqual("amd64", cfg.Targets[0].Arch) {
- t.Fatalf("want %v, got %v", "amd64", cfg.Targets[0].Arch)
- }
-
- })
-
- t.Run("defaults to the local medium when nil is passed", func(t *testing.T) {
- dir := t.TempDir()
- configPath := ax.Join(dir, "custom-build.yaml")
- content := `
-version: 1
-project:
- name: explicit-nil-medium
-`
- requireConfigOKResult(t, ax.WriteFile(configPath, []byte(content), 0o644))
-
- cfg := requireConfigOK(t, LoadConfigAtPath(nil, configPath))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("explicit-nil-medium", cfg.Project.Name) {
- t.Fatalf("want %v, got %v", "explicit-nil-medium", cfg.Project.Name)
- }
-
- })
-}
-
-func TestConfig_ConfigExistsNilMediumGood(t *testing.T) {
- t.Run("returns false for a nil medium", func(t *testing.T) {
- if ConfigExists(nil, t.TempDir()) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestConfig_LoadConfig_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("returns error for invalid YAML", func(t *testing.T) {
- content := `
-version: 1
-project:
- name: [invalid yaml
-`
- dir := setupConfigTestDir(t, content)
-
- err := requireConfigError(t, LoadConfig(fs, dir))
- if !stdlibAssertContains(err, "failed to parse config file") {
- t.Fatalf("expected %v to contain %v", err, "failed to parse config file")
- }
-
- })
-
- t.Run("returns error for unreadable file", func(t *testing.T) {
- dir := t.TempDir()
- coreDir := ax.Join(dir, ConfigDir)
- requireConfigOKResult(t, ax.MkdirAll(coreDir, 0755))
-
- // Create config as a directory instead of file.
- configPath := ax.Join(coreDir, ConfigFileName)
- requireConfigOKResult(t, ax.Mkdir(configPath, 0755))
-
- err := requireConfigError(t, LoadConfig(fs, dir))
- if !stdlibAssertContains(err, "failed to read config file") {
- t.Fatalf("expected %v to contain %v", err, "failed to read config file")
- }
-
- })
-}
-
-func TestConfig_DefaultConfig_Good(t *testing.T) {
- t.Run("returns sensible defaults", func(t *testing.T) {
- cfg := DefaultConfig()
- if !stdlibAssertEqual(1, cfg.Version) {
- t.Fatalf("want %v, got %v", 1, cfg.Version)
- }
- if !stdlibAssertEqual(".", cfg.Project.Main) {
- t.Fatalf("want %v, got %v", ".", cfg.Project.Main)
- }
- if !stdlibAssertEmpty(cfg.Project.Name) {
- t.Fatalf("expected empty, got %v", cfg.Project.Name)
- }
- if !stdlibAssertEmpty(cfg.Project.Binary) {
- t.Fatalf("expected empty, got %v", cfg.Project.Binary)
- }
- if cfg.Build.CGO {
- t.Fatal("expected false")
- }
- if !stdlibAssertContains(cfg.Build.Flags, "-trimpath") {
- t.Fatalf("expected %v to contain %v", cfg.Build.Flags, "-trimpath")
- }
- if !stdlibAssertContains(cfg.
-
- // Default targets cover common platforms
- Build.LDFlags, "-s") {
- t.Fatalf("expected %v to contain %v", cfg.Build.LDFlags, "-s")
- }
- if !stdlibAssertContains(cfg.Build.LDFlags, "-w") {
- t.Fatalf("expected %v to contain %v", cfg.Build.LDFlags, "-w")
- }
- if !stdlibAssertEmpty(cfg.Build.Env) {
- t.Fatalf("expected empty, got %v", cfg.Build.Env)
- }
- if !stdlibAssertEqual("core-dev", cfg.LinuxKit.Base) {
- t.Fatalf("want %v, got %v", "core-dev", cfg.LinuxKit.Base)
- }
- if !stdlibAssertEqual([]string{"/workspace"}, cfg.LinuxKit.Mounts) {
- t.Fatalf("want %v, got %v", []string{"/workspace"}, cfg.LinuxKit.Mounts)
- }
- if !stdlibAssertEqual([]string{"oci", "apple"}, cfg.LinuxKit.Formats) {
- t.Fatalf("want %v, got %v", []string{"oci", "apple"}, cfg.LinuxKit.Formats)
- }
- if len(cfg.Targets) != 5 {
- t.Fatalf("want len %v, got %v", 5, len(cfg.Targets))
- }
-
- hasLinuxAmd64 := false
- hasDarwinAmd64 := false
- hasDarwinArm64 := false
- hasWindowsAmd64 := false
- for _, t := range cfg.Targets {
- if t.OS == "linux" && t.Arch == "amd64" {
- hasLinuxAmd64 = true
- }
- if t.OS == "darwin" && t.Arch == "amd64" {
- hasDarwinAmd64 = true
- }
- if t.OS == "darwin" && t.Arch == "arm64" {
- hasDarwinArm64 = true
- }
- if t.OS == "windows" && t.Arch == "amd64" {
- hasWindowsAmd64 = true
- }
- }
- if !(hasLinuxAmd64) {
- t.Fatal("expected true")
- }
- if !(hasDarwinAmd64) {
- t.Fatal("expected true")
- }
- if !(hasDarwinArm64) {
- t.Fatal("expected true")
- }
- if !(hasWindowsAmd64) {
- t.Fatal("expected true")
- }
-
- })
-}
-
-func TestConfig_CloneBuildConfig_Good(t *testing.T) {
- sign := true
- notarise := false
- dmg := true
-
- cfg := &BuildConfig{
- Build: Build{
- Flags: []string{"-trimpath"},
- LDFlags: []string{"-s", "-w"},
- BuildTags: []string{"integration"},
- Env: []string{"FOO=bar"},
- Cache: CacheConfig{Enabled: true, Directory: ".core/cache", Paths: []string{"cache/go-build"}, RestoreKeys: []string{"main"}},
- Tags: []string{"latest"},
- BuildArgs: map[string]string{"VERSION": "v1.2.3"},
- Formats: []string{"iso"},
- },
- LinuxKit: LinuxKitConfig{
- Base: "core-dev",
- Packages: []string{"git"},
- Mounts: []string{"/workspace"},
- GPU: true,
- Formats: []string{"oci", "apple"},
- Registry: "ghcr.io/dappcore",
- },
- Apple: AppleConfig{
- Sign: &sign,
- Notarise: ¬arise,
- DMG: &dmg,
- XcodeCloud: XcodeCloudConfig{
- Workflow: "Release",
- Triggers: []XcodeCloudTrigger{{Branch: "main", Action: "testflight"}},
- },
- },
- SDK: &sdk.Config{
- Spec: "docs/openapi.yaml",
- Languages: []string{"typescript"},
- Output: "generated/sdk",
- },
- Targets: []TargetConfig{{OS: "linux", Arch: "amd64"}},
- }
-
- clone := CloneBuildConfig(cfg)
- if stdlibAssertNil(clone) {
- t.Fatal("expected non-nil")
- }
-
- clone.Build.Flags[0] = "-mod=readonly"
- clone.Build.LDFlags[0] = "-X"
- clone.Build.BuildTags[0] = "release"
- clone.Build.Env[0] = "BAR=baz"
- clone.Build.Cache.Paths[0] = "cache/go-mod"
- clone.Build.Cache.RestoreKeys[0] = "fallback"
- clone.Build.Tags[0] = "stable"
- clone.Build.BuildArgs["VERSION"] = "v2.0.0"
- clone.Build.Formats[0] = "qcow2"
- clone.LinuxKit.Base = "core-minimal"
- clone.LinuxKit.Packages[0] = "task"
- clone.LinuxKit.Mounts[0] = "/src"
- clone.LinuxKit.Formats[0] = "tar"
- clone.LinuxKit.Registry = "registry.example.com/core"
- *clone.Apple.Sign = false
- *clone.Apple.Notarise = true
- *clone.Apple.DMG = false
- clone.Apple.XcodeCloud.Triggers[0].Branch = "dev"
- clone.SDK.Languages[0] = "python"
- clone.SDK.Output = "sdk"
- clone.Targets[0].OS = "darwin"
- if !stdlibAssertEqual([]string{"-trimpath"}, cfg.Build.Flags) {
- t.Fatalf("want %v, got %v", []string{"-trimpath"}, cfg.Build.Flags)
- }
- if !stdlibAssertEqual([]string{"-s", "-w"}, cfg.Build.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w"}, cfg.Build.LDFlags)
- }
- if !stdlibAssertEqual([]string{"integration"}, cfg.Build.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"integration"}, cfg.Build.BuildTags)
- }
- if !stdlibAssertEqual([]string{"FOO=bar"}, cfg.Build.Env) {
- t.Fatalf("want %v, got %v", []string{"FOO=bar"}, cfg.Build.Env)
- }
- if !stdlibAssertEqual([]string{"cache/go-build"}, cfg.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{"cache/go-build"}, cfg.Build.Cache.Paths)
- }
- if !stdlibAssertEqual([]string{"main"}, cfg.Build.Cache.RestoreKeys) {
- t.Fatalf("want %v, got %v", []string{"main"}, cfg.Build.Cache.RestoreKeys)
- }
- if !stdlibAssertEqual([]string{"latest"}, cfg.Build.Tags) {
- t.Fatalf("want %v, got %v", []string{"latest"}, cfg.Build.Tags)
- }
- if !stdlibAssertEqual(map[string]string{"VERSION": "v1.2.3"}, cfg.Build.BuildArgs) {
- t.Fatalf("want %v, got %v", map[string]string{"VERSION": "v1.2.3"}, cfg.Build.BuildArgs)
- }
- if !stdlibAssertEqual([]string{"iso"}, cfg.Build.Formats) {
- t.Fatalf("want %v, got %v", []string{"iso"}, cfg.Build.Formats)
- }
- if !stdlibAssertEqual("core-dev", cfg.LinuxKit.Base) {
- t.Fatalf("want %v, got %v", "core-dev", cfg.LinuxKit.Base)
- }
- if !stdlibAssertEqual([]string{"git"}, cfg.LinuxKit.Packages) {
- t.Fatalf("want %v, got %v", []string{"git"}, cfg.LinuxKit.Packages)
- }
- if !stdlibAssertEqual([]string{"/workspace"}, cfg.LinuxKit.Mounts) {
- t.Fatalf("want %v, got %v", []string{"/workspace"}, cfg.LinuxKit.Mounts)
- }
- if !stdlibAssertEqual([]string{"oci", "apple"}, cfg.LinuxKit.Formats) {
- t.Fatalf("want %v, got %v", []string{"oci", "apple"}, cfg.LinuxKit.Formats)
- }
- if !stdlibAssertEqual("ghcr.io/dappcore", cfg.LinuxKit.Registry) {
- t.Fatalf("want %v, got %v", "ghcr.io/dappcore", cfg.LinuxKit.Registry)
- }
- if stdlibAssertNil(cfg.Apple.Sign) {
- t.Fatal("expected non-nil")
- }
- if stdlibAssertNil(cfg.Apple.Notarise) {
- t.Fatal("expected non-nil")
- }
- if stdlibAssertNil(cfg.Apple.DMG) {
- t.Fatal("expected non-nil")
- }
- if !(*cfg.Apple.Sign) {
- t.Fatal("expected true")
- }
- if *cfg.Apple.Notarise {
- t.Fatal("expected false")
- }
- if !(*cfg.Apple.DMG) {
- t.Fatal("expected true")
- }
- if len(cfg.Apple.XcodeCloud.Triggers) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(cfg.Apple.XcodeCloud.Triggers))
- }
- if !stdlibAssertEqual("main", cfg.Apple.XcodeCloud.Triggers[0].Branch) {
- t.Fatalf("want %v, got %v", "main", cfg.Apple.XcodeCloud.Triggers[0].Branch)
- }
- if stdlibAssertNil(cfg.SDK) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual([]string{"typescript"}, cfg.SDK.Languages) {
- t.Fatalf("want %v, got %v", []string{"typescript"}, cfg.SDK.Languages)
- }
- if !stdlibAssertEqual("generated/sdk", cfg.SDK.Output) {
- t.Fatalf("want %v, got %v", "generated/sdk", cfg.SDK.Output)
- }
- if !stdlibAssertEqual([]TargetConfig{{OS: "linux", Arch: "amd64"}}, cfg.Targets) {
- t.Fatalf("want %v, got %v", []TargetConfig{{OS: "linux", Arch: "amd64"}}, cfg.Targets)
- }
-
-}
-
-func TestConfig_ConfigPath_Good(t *testing.T) {
- t.Run("returns correct path", func(t *testing.T) {
- path := ConfigPath("/project/root")
- if !stdlibAssertEqual("/project/root/.core/build.yaml", path) {
- t.Fatalf("want %v, got %v", "/project/root/.core/build.yaml", path)
- }
-
- })
-}
-
-func TestConfig_ConfigExists_Good(t *testing.T) {
- fs := storage.Local
- t.Run("returns true when config exists", func(t *testing.T) {
- dir := setupConfigTestDir(t, "version: 1")
- if !(ConfigExists(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false when config missing", func(t *testing.T) {
- dir := t.TempDir()
- if ConfigExists(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false when .core dir missing", func(t *testing.T) {
- dir := t.TempDir()
- if ConfigExists(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestConfig_LoadConfigSignConfigGood(t *testing.T) {
- tmpDir := t.TempDir()
- coreDir := ax.Join(tmpDir, ".core")
- requireConfigOKResult(t, ax.MkdirAll(coreDir, 0755))
-
- configContent := `version: 1
-sign:
- enabled: true
- gpg:
- key: "ABCD1234"
- macos:
- identity: "Developer ID Application: Test"
- notarize: true
-`
- requireConfigOKResult(t, ax.WriteFile(ax.Join(coreDir, "build.yaml"), []byte(configContent), 0644))
-
- cfg := requireConfigOK(t, LoadConfig(storage.Local, tmpDir))
-
- if !cfg.Sign.Enabled {
- t.Error("expected Sign.Enabled to be true")
- }
- if cfg.Sign.GPG.Key != "ABCD1234" {
- t.Errorf("expected GPG.Key 'ABCD1234', got %q", cfg.Sign.GPG.Key)
- }
- if cfg.Sign.MacOS.Identity != "Developer ID Application: Test" {
- t.Errorf("expected MacOS.Identity, got %q", cfg.Sign.MacOS.Identity)
- }
- if !cfg.Sign.MacOS.Notarize {
- t.Error("expected MacOS.Notarize to be true")
- }
-}
-
-func TestConfig_BuildConfigToTargetsGood(t *testing.T) {
- t.Run("converts TargetConfig to Target", func(t *testing.T) {
- cfg := &BuildConfig{
- Targets: []TargetConfig{
- {OS: "linux", Arch: "amd64"},
- {OS: "darwin", Arch: "arm64"},
- {OS: "windows", Arch: "386"},
- },
- }
-
- targets := cfg.ToTargets()
- if len(targets) != 3 {
- t.Fatalf("want len %v, got %v", 3, len(targets))
- }
- if !stdlibAssertEqual(Target{OS: "linux", Arch: "amd64"}, targets[0]) {
- t.Fatalf("want %v, got %v", Target{OS: "linux", Arch: "amd64"}, targets[0])
- }
- if !stdlibAssertEqual(Target{OS: "darwin", Arch: "arm64"}, targets[1]) {
- t.Fatalf("want %v, got %v", Target{OS: "darwin", Arch: "arm64"}, targets[1])
- }
- if !stdlibAssertEqual(Target{OS: "windows", Arch: "386"}, targets[2]) {
- t.Fatalf("want %v, got %v",
-
- // TestLoadConfig_Testdata tests loading from the testdata fixture.
- Target{OS: "windows", Arch: "386"}, targets[2])
- }
-
- })
-
- t.Run("returns empty slice for no targets", func(t *testing.T) {
- cfg := &BuildConfig{
- Targets: []TargetConfig{},
- }
-
- targets := cfg.ToTargets()
- if !stdlibAssertEmpty(targets) {
- t.Fatalf("expected empty, got %v", targets)
- }
-
- })
-}
-
-func TestConfig_LoadConfigTestdataGood(t *testing.T) {
- fs := storage.Local
- abs := requireConfigString(t, ax.Abs("testdata/config-project"))
-
- t.Run("loads config-project fixture", func(t *testing.T) {
- cfg := requireConfigOK(t, LoadConfig(fs, abs))
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(1, cfg.Version) {
- t.Fatalf("want %v, got %v", 1, cfg.Version)
- }
- if !stdlibAssertEqual("example-cli", cfg.Project.Name) {
- t.Fatalf("want %v, got %v", "example-cli", cfg.Project.Name)
- }
- if !stdlibAssertEqual("An example CLI application", cfg.Project.Description) {
- t.Fatalf("want %v, got %v", "An example CLI application", cfg.Project.Description)
- }
- if !stdlibAssertEqual("./cmd/example", cfg.Project.Main) {
- t.Fatalf("want %v, got %v", "./cmd/example", cfg.Project.Main)
- }
- if !stdlibAssertEqual("example", cfg.Project.Binary) {
- t.Fatalf("want %v, got %v", "example", cfg.Project.Binary)
- }
- if cfg.Build.CGO {
- t.Fatal("expected false")
- }
- if !stdlibAssertEqual([]string{"-trimpath"}, cfg.Build.Flags) {
- t.Fatalf("want %v, got %v", []string{"-trimpath"}, cfg.Build.Flags)
- }
- if !stdlibAssertEqual([]string{"-s", "-w"}, cfg.Build.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w"}, cfg.Build.LDFlags)
- }
- if len(cfg.Targets) != 3 {
- t.Fatalf("want len %v, got %v", 3, len(cfg.Targets))
- }
-
- })
-}
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
-
-// --- v0.9.0 generated compliance triplets ---
-func TestConfig_BuildConfig_UnmarshalYAML_Good(t *core.T) {
- subject := &BuildConfig{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.UnmarshalYAML(&yaml.Node{Kind: yaml.ScalarNode, Value: "false"})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestConfig_BuildConfig_UnmarshalYAML_Bad(t *core.T) {
- subject := &BuildConfig{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.UnmarshalYAML(&yaml.Node{Kind: yaml.ScalarNode, Value: "false"})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_BuildConfig_UnmarshalYAML_Ugly(t *core.T) {
- subject := &BuildConfig{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.UnmarshalYAML(&yaml.Node{Kind: yaml.ScalarNode, Value: "false"})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_BuildConfig_MarshalYAML_Good(t *core.T) {
- subject := BuildConfig{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.MarshalYAML()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestConfig_BuildConfig_MarshalYAML_Bad(t *core.T) {
- subject := BuildConfig{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.MarshalYAML()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_BuildConfig_MarshalYAML_Ugly(t *core.T) {
- subject := BuildConfig{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.MarshalYAML()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_TargetConfig_MarshalYAML_Good(t *core.T) {
- raw := requireConfigMap((*testing.T)(t), (TargetConfig{OS: "linux", Arch: "amd64"}).MarshalYAML())
- core.AssertEqual(t, "linux", raw[targetConfigOSField])
- core.AssertEqual(t, "amd64", raw["arch"])
-}
-
-func TestConfig_TargetConfig_MarshalYAML_Bad(t *core.T) {
- raw := requireConfigMap((*testing.T)(t), (TargetConfig{}).MarshalYAML())
- core.AssertEqual(t, "", raw[targetConfigOSField])
- core.AssertEqual(t, "", raw["arch"])
-}
-
-func TestConfig_TargetConfig_MarshalYAML_Ugly(t *core.T) {
- raw := requireConfigMap((*testing.T)(t), (TargetConfig{OS: "darwin", Arch: "arm64/v8"}).MarshalYAML())
- core.AssertEqual(t, "darwin", raw[targetConfigOSField])
- core.AssertEqual(t, "arm64/v8", raw["arch"])
-}
-
-func TestConfig_TargetConfig_UnmarshalYAML_Good(t *core.T) {
- node := &yaml.Node{}
- core.RequireNoError(t, node.Encode(map[string]string{targetConfigOSField: "linux", "arch": "amd64"}))
- var subject TargetConfig
- result := subject.UnmarshalYAML(node)
- core.RequireTrue(t, result.OK)
- core.AssertEqual(t, "linux", subject.OS)
- core.AssertEqual(t, "amd64", subject.Arch)
-}
-
-func TestConfig_TargetConfig_UnmarshalYAML_Bad(t *core.T) {
- var subject TargetConfig
- result := subject.UnmarshalYAML(&yaml.Node{Kind: yaml.ScalarNode, Value: "not-a-map"})
- core.AssertFalse(t, result.OK)
-}
-
-func TestConfig_TargetConfig_UnmarshalYAML_Ugly(t *core.T) {
- node := &yaml.Node{}
- core.RequireNoError(t, node.Encode(map[string]string{targetConfigOSField: "windows", "arch": "arm64", "ignored": "yes"}))
- var subject TargetConfig
- result := subject.UnmarshalYAML(node)
- core.RequireTrue(t, result.OK)
- core.AssertEqual(t, "windows", subject.OS)
- core.AssertEqual(t, "arm64", subject.Arch)
-}
-
-func TestConfig_LoadConfig_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = LoadConfig(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_LoadConfigAtPath_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = LoadConfigAtPath(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_LoadConfigAtPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = LoadConfigAtPath(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_DefaultConfig_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultConfig()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_DefaultConfig_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultConfig()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_ResolveOutputMedium_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveOutputMedium(&Config{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestConfig_ResolveOutputMedium_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveOutputMedium(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_ResolveOutputMedium_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveOutputMedium(&Config{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_MediumIsLocal_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = MediumIsLocal(storage.NewMemoryMedium())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestConfig_MediumIsLocal_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = MediumIsLocal(storage.NewMemoryMedium())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_MediumIsLocal_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = MediumIsLocal(storage.NewMemoryMedium())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_CopyMediumPath_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CopyMediumPath(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestConfig_CopyMediumPath_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CopyMediumPath(storage.NewMemoryMedium(), "", storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_CopyMediumPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CopyMediumPath(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_BuildConfig_ExpandEnv_Good(t *core.T) {
- subject := &BuildConfig{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- subject.ExpandEnv()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestConfig_BuildConfig_ExpandEnv_Bad(t *core.T) {
- subject := &BuildConfig{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- subject.ExpandEnv()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_BuildConfig_ExpandEnv_Ugly(t *core.T) {
- subject := &BuildConfig{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- subject.ExpandEnv()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_CloneStringMap_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CloneStringMap(nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestConfig_CloneStringMap_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CloneStringMap(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_CloneStringMap_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CloneStringMap(nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_CloneBuildConfig_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CloneBuildConfig(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_CloneBuildConfig_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = CloneBuildConfig(&BuildConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_ConfigPath_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ConfigPath("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_ConfigPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ConfigPath(core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_ConfigExists_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ConfigExists(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_ConfigExists_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ConfigExists(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_BuildConfig_TargetsIter_Good(t *core.T) {
- subject := &BuildConfig{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.TargetsIter()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestConfig_BuildConfig_TargetsIter_Bad(t *core.T) {
- subject := &BuildConfig{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.TargetsIter()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_BuildConfig_TargetsIter_Ugly(t *core.T) {
- subject := &BuildConfig{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.TargetsIter()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestConfig_BuildConfig_ToTargets_Good(t *core.T) {
- subject := &BuildConfig{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ToTargets()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestConfig_BuildConfig_ToTargets_Bad(t *core.T) {
- subject := &BuildConfig{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ToTargets()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestConfig_BuildConfig_ToTargets_Ugly(t *core.T) {
- subject := &BuildConfig{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ToTargets()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/discovery.go b/pkg/build/discovery.go
deleted file mode 100644
index 7b4d4d9..0000000
--- a/pkg/build/discovery.go
+++ /dev/null
@@ -1,944 +0,0 @@
-package build
-
-import (
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// Marker files for project type detection.
-const (
- markerBuildConfig = ".core/build.yaml"
- markerGoMod = "go.mod"
- markerGoWork = "go.work"
- markerMainGo = "main.go"
- markerWails = "wails.json"
- markerNodePackage = "package.json"
- markerDenoJSON = "deno.json"
- markerDenoJSONC = "deno.jsonc"
- markerComposer = "composer.json"
- markerMkDocs = "mkdocs.yml"
- markerMkDocsYAML = "mkdocs.yaml"
- markerDocsMkDocs = "docs/mkdocs.yml"
- markerDocsMkDocsYAML = "docs/mkdocs.yaml"
- markerPyProject = "pyproject.toml"
- markerRequirements = "requirements.txt"
- markerCargo = "Cargo.toml"
- markerDockerfile = "Dockerfile"
- markerFrontendPackage = "frontend/package.json"
- markerFrontendDenoJSON = "frontend/deno.json"
- markerFrontendDenoJSONC = "frontend/deno.jsonc"
- markerLinuxKitYAML = "linuxkit.yml"
- markerLinuxKitYAMLAlt = "linuxkit.yaml"
- markerTaskfileYML = "Taskfile.yml"
- markerTaskfileYAML = "Taskfile.yaml"
- markerTaskfileBare = "Taskfile"
- markerTaskfileLowerYML = "taskfile.yml"
- markerTaskfileLowerYAML = "taskfile.yaml"
- markerLinuxKitNestedYML = ".core/linuxkit/*.yml"
- markerLinuxKitNestedYAML = ".core/linuxkit/*.yaml"
-)
-
-type discoveryRule struct {
- projectType ProjectType
- matches func(storage.Medium, string) bool
-}
-
-var discoveryRules = []discoveryRule{
- {projectType: ProjectTypeWails, matches: IsWailsProject},
- {projectType: ProjectTypeGo, matches: func(fs storage.Medium, dir string) bool {
- return fileExists(fs, ax.Join(dir, markerGoMod)) || fileExists(fs, ax.Join(dir, markerGoWork))
- }},
- {projectType: ProjectTypeNode, matches: IsNodeProject},
- {projectType: ProjectTypePHP, matches: IsPHPProject},
- {projectType: ProjectTypePython, matches: IsPythonProject},
- {projectType: ProjectTypeRust, matches: IsRustProject},
- {projectType: ProjectTypeCPP, matches: IsCPPProject},
- {projectType: ProjectTypeDocker, matches: IsDockerProject},
- {projectType: ProjectTypeLinuxKit, matches: IsLinuxKitProject},
- {projectType: ProjectTypeTaskfile, matches: IsTaskfileProject},
- {projectType: ProjectTypeDocs, matches: IsDocsProject},
-}
-
-var discoveryMarkerPaths = []string{
- markerBuildConfig,
- markerGoMod, markerGoWork, markerMainGo, markerWails, markerNodePackage, markerDenoJSON, markerDenoJSONC, markerComposer,
- markerMkDocs, markerMkDocsYAML, markerDocsMkDocs, markerDocsMkDocsYAML,
- markerPyProject, markerRequirements, markerCargo,
- "CMakeLists.txt", markerDockerfile, "Containerfile", "dockerfile", "containerfile",
- markerFrontendPackage, markerFrontendDenoJSON, markerFrontendDenoJSONC,
- markerLinuxKitYAML, markerLinuxKitYAMLAlt,
- markerTaskfileYML, markerTaskfileYAML, markerTaskfileBare,
- markerTaskfileLowerYML, markerTaskfileLowerYAML,
-}
-
-// Discover detects project types in the given directory by checking for marker files.
-// Returns a slice of detected project types, ordered by priority (most specific first).
-// For example, a Wails project returns [wails, go] since it has both wails.json and go.mod.
-//
-// types, err := build.Discover(storage.Local, "/home/user/my-project") // → [go]
-func Discover(fs storage.Medium, dir string) core.Result {
- var detected []ProjectType
-
- if configuredType, ok := configuredProjectType(fs, dir); ok {
- return core.Ok([]ProjectType{configuredType})
- }
-
- appendType := func(projectType ProjectType, ok bool) {
- if !ok || core.NewArray(detected...).Contains(projectType) {
- return
- }
- detected = append(detected, projectType)
- }
-
- for _, rule := range discoveryRules {
- appendType(rule.projectType, rule.matches(fs, dir))
- }
-
- return core.Ok(detected)
-}
-
-// PrimaryType returns the most specific project type detected in the directory.
-// Returns empty string if no project type is detected.
-//
-// pt, err := build.PrimaryType(storage.Local, ".") // → "go"
-func PrimaryType(fs storage.Medium, dir string) core.Result {
- typesResult := Discover(fs, dir)
- if !typesResult.OK {
- return typesResult
- }
- types := typesResult.Value.([]ProjectType)
- if len(types) == 0 {
- return core.Ok(ProjectType(""))
- }
- return core.Ok(types[0])
-}
-
-// IsGoProject checks if the directory contains a Go project (go.mod, go.work, or wails.json).
-//
-// if build.IsGoProject(storage.Local, ".") { ... }
-func IsGoProject(fs storage.Medium, dir string) bool {
- return fileExists(fs, ax.Join(dir, markerGoMod)) ||
- fileExists(fs, ax.Join(dir, markerGoWork)) ||
- fileExists(fs, ax.Join(dir, markerWails))
-}
-
-// IsWailsProject checks if the directory contains a Wails project.
-//
-// if build.IsWailsProject(storage.Local, ".") { ... }
-func IsWailsProject(fs storage.Medium, dir string) bool {
- if fileExists(fs, ax.Join(dir, markerWails)) {
- return true
- }
-
- if !hasGoRootMarker(fs, dir) {
- return false
- }
-
- return hasFrontendManifest(fs, dir) ||
- hasFrontendManifest(fs, ax.Join(dir, "frontend")) ||
- hasSubtreeFrontendManifest(fs, dir)
-}
-
-// IsNodeProject checks if the directory contains a Node.js or Deno frontend
-// project at the root, under frontend/, or in a visible nested subtree.
-//
-// if build.IsNodeProject(storage.Local, ".") { ... }
-func IsNodeProject(fs storage.Medium, dir string) bool {
- return hasFrontendManifest(fs, dir) ||
- hasFrontendManifest(fs, ax.Join(dir, "frontend")) ||
- hasSubtreeFrontendManifest(fs, dir)
-}
-
-// IsPHPProject checks if the directory contains a PHP project.
-//
-// if build.IsPHPProject(storage.Local, ".") { ... }
-func IsPHPProject(fs storage.Medium, dir string) bool {
- return fileExists(fs, ax.Join(dir, markerComposer))
-}
-
-// IsCPPProject checks if the directory contains a C++ project (CMakeLists.txt).
-//
-// if build.IsCPPProject(storage.Local, ".") { ... }
-func IsCPPProject(fs storage.Medium, dir string) bool {
- return fileExists(fs, ax.Join(dir, "CMakeLists.txt"))
-}
-
-// IsMkDocsProject checks for MkDocs config at the project root or in docs/.
-//
-// ok := build.IsMkDocsProject(storage.Local, ".")
-func IsMkDocsProject(fs storage.Medium, dir string) bool {
- return ResolveMkDocsConfigPath(fs, dir) != ""
-}
-
-// IsDocsProject is the predictable alias for IsMkDocsProject.
-//
-// ok := build.IsDocsProject(storage.Local, ".")
-func IsDocsProject(fs storage.Medium, dir string) bool {
- return IsMkDocsProject(fs, dir)
-}
-
-// ResolveMkDocsConfigPath returns the first MkDocs config path that exists.
-//
-// configPath := build.ResolveMkDocsConfigPath(storage.Local, ".")
-func ResolveMkDocsConfigPath(fs storage.Medium, dir string) string {
- for _, path := range []string{
- ax.Join(dir, markerMkDocs),
- ax.Join(dir, markerMkDocsYAML),
- ax.Join(dir, "docs", "mkdocs.yml"),
- ax.Join(dir, "docs", "mkdocs.yaml"),
- } {
- if fileExists(fs, path) {
- return path
- }
- }
-
- if path := findMkDocsConfigInSubtree(fs, dir, 0); path != "" {
- return path
- }
-
- return ""
-}
-
-// HasSubtreeNpm checks for package.json within depth 2 subdirectories.
-// Ignores root package.json, the conventional frontend/ directory, hidden
-// directories, and node_modules directories.
-// Returns true when a monorepo-style nested package.json is found.
-//
-// ok := build.HasSubtreeNpm(storage.Local, ".") // true if apps/web/package.json exists
-func HasSubtreeNpm(fs storage.Medium, dir string) bool {
- if fs == nil {
- return false
- }
-
- // Depth 1: list immediate subdirectories
- entriesResult := fs.List(dir)
- if !entriesResult.OK {
- return false
- }
-
- for _, entry := range entriesResult.Value.([]core.FsDirEntry) {
- if !entry.IsDir() {
- continue
- }
- name := entry.Name()
- if shouldSkipSubtreeDir(name) || name == "frontend" {
- continue
- }
-
- subdir := ax.Join(dir, name)
-
- // Depth 1: check subdir/package.json
- if fileExists(fs, ax.Join(subdir, markerNodePackage)) {
- return true
- }
-
- // Depth 2: list subdirectories of subdir
- subEntriesResult := fs.List(subdir)
- if !subEntriesResult.OK {
- continue
- }
- for _, subEntry := range subEntriesResult.Value.([]core.FsDirEntry) {
- if !subEntry.IsDir() {
- continue
- }
- if shouldSkipSubtreeDir(subEntry.Name()) {
- continue
- }
- nested := ax.Join(subdir, subEntry.Name())
- if fileExists(fs, ax.Join(nested, markerNodePackage)) {
- return true
- }
- }
- }
-
- return false
-}
-
-// IsPythonProject checks for pyproject.toml or requirements.txt at the project root.
-//
-// ok := build.IsPythonProject(storage.Local, ".")
-func IsPythonProject(fs storage.Medium, dir string) bool {
- return fileExists(fs, ax.Join(dir, markerPyProject)) ||
- fileExists(fs, ax.Join(dir, markerRequirements))
-}
-
-// IsRustProject checks for Cargo.toml at the project root.
-//
-// ok := build.IsRustProject(storage.Local, ".")
-func IsRustProject(fs storage.Medium, dir string) bool {
- return fileExists(fs, ax.Join(dir, markerCargo))
-}
-
-// DiscoveryResult holds the full project analysis from DiscoverFull().
-//
-// result, err := build.DiscoverFull(storage.Local, ".")
-// fmt.Println(result.PrimaryStack) // "wails"
-type DiscoveryResult struct {
- // Types lists all detected project types in priority order.
- Types []ProjectType
- // ConfiguredType is the explicit build.type override from .core/build.yaml when present.
- ConfiguredType string
- // ConfiguredBuildType mirrors the workflow-facing discovery output name.
- ConfiguredBuildType string
- // OS is the current host operating system for the discovery run.
- OS string
- // Arch is the current host architecture for the discovery run.
- Arch string
- // PrimaryStack is the best stack suggestion based on detected types.
- PrimaryStack string
- // SuggestedStack is the richer action-oriented stack hint derived from markers.
- // This preserves the v3 action naming where Wails projects map to "wails2".
- SuggestedStack string
- // HasFrontend is true when a root or frontend/ package.json/deno manifest is found,
- // or when a nested frontend tree is detected.
- HasFrontend bool
- // HasRootPackageJSON reports whether package.json exists at the project root.
- HasRootPackageJSON bool
- // HasFrontendPackageJSON reports whether frontend/package.json exists.
- HasFrontendPackageJSON bool
- // HasRootComposerJSON reports whether composer.json exists at the project root.
- HasRootComposerJSON bool
- // HasRootCargoToml reports whether Cargo.toml exists at the project root.
- HasRootCargoToml bool
- // HasRootGoMod reports whether go.mod exists at the project root.
- HasRootGoMod bool
- // HasRootGoWork reports whether go.work exists at the project root.
- HasRootGoWork bool
- // HasRootMainGo reports whether main.go exists at the project root.
- HasRootMainGo bool
- // HasRootCMakeLists reports whether CMakeLists.txt exists at the project root.
- HasRootCMakeLists bool
- // HasRootWailsJSON reports whether wails.json exists at the project root.
- HasRootWailsJSON bool
- // HasPackageJSON reports whether package.json exists at the root, in frontend/,
- // or in a supported nested subtree.
- HasPackageJSON bool
- // HasDenoManifest reports whether deno.json or deno.jsonc exists at the root,
- // in frontend/, or in a supported nested subtree.
- HasDenoManifest bool
- // HasTaskfile reports whether any supported Taskfile name exists at the project root.
- HasTaskfile bool
- // HasSubtreeNpm is true when a nested package.json exists within depth 2.
- HasSubtreeNpm bool
- // HasSubtreePackageJSON mirrors the workflow-facing discovery output name.
- HasSubtreePackageJSON bool
- // HasSubtreeDenoManifest is true when a nested Deno manifest exists within depth 2.
- HasSubtreeDenoManifest bool
- // HasDocsConfig reports whether MkDocs config exists at the root or under docs/.
- HasDocsConfig bool
- // HasGoToolchain reports whether Go markers exist at the root or in a visible
- // nested subtree, mirroring the action discovery contract used for setup.
- HasGoToolchain bool
- // PrimaryStackSuggestion mirrors the richer action output name and marker-based
- // precedence used by the generated workflow discovery step.
- PrimaryStackSuggestion string
- // LinuxPackages lists distro-aware system dependencies needed by the detected stack.
- LinuxPackages []string
- // WebKitPackage is the Ubuntu-aware WebKit dependency selected for Wails builds.
- WebKitPackage string
- // Markers records the presence of each raw marker file checked.
- Markers map[string]bool
- // Distro holds the detected Linux distribution version (e.g., "24.04").
- // Used by ComputeOptions to inject webkit2_41 tag on Ubuntu 24.04+.
- Distro string
- // Ref is the Git ref when discovery runs under GitHub metadata.
- Ref string
- // Branch is the branch name when available from GitHub metadata.
- Branch string
- // Tag is the tag name when available from GitHub metadata.
- Tag string
- // IsTag reports whether Ref points at a tag.
- IsTag bool
- // SHA is the current GitHub commit SHA when available.
- SHA string
- // ShortSHA is the short GitHub commit SHA when available.
- ShortSHA string
- // Repo is the GitHub owner/repo string when available.
- Repo string
- // Owner is the GitHub repository owner when available.
- Owner string
-}
-
-// DiscoverFull returns a rich discovery result with all markers and metadata.
-//
-// result, err := build.DiscoverFull(storage.Local, ".")
-// if result.HasFrontend { ... }
-func DiscoverFull(fs storage.Medium, dir string) core.Result {
- typesResult := Discover(fs, dir)
- if !typesResult.OK {
- return typesResult
- }
- types := typesResult.Value.([]ProjectType)
-
- result := &DiscoveryResult{
- Types: types,
- OS: discoverHostOS(),
- Arch: discoverHostArch(),
- Markers: make(map[string]bool),
- }
-
- // Record raw marker presence
- result.Markers = collectMarkerPresence(fs, dir, discoveryMarkerPaths)
-
- result.HasRootPackageJSON = result.Markers[markerNodePackage]
- result.HasFrontendPackageJSON = result.Markers[markerFrontendPackage]
- result.HasRootComposerJSON = result.Markers[markerComposer]
- result.HasRootCargoToml = result.Markers[markerCargo]
- result.HasRootGoMod = result.Markers[markerGoMod]
- result.HasRootGoWork = result.Markers[markerGoWork]
- result.HasRootMainGo = result.Markers[markerMainGo]
- result.HasRootCMakeLists = result.Markers["CMakeLists.txt"]
- result.HasRootWailsJSON = result.Markers[markerWails]
- result.HasTaskfile = result.Markers[markerTaskfileYML] ||
- result.Markers[markerTaskfileYAML] ||
- result.Markers[markerTaskfileBare] ||
- result.Markers[markerTaskfileLowerYML] ||
- result.Markers[markerTaskfileLowerYAML]
- result.HasDocsConfig = IsMkDocsProject(fs, dir)
-
- // Pattern-based marker: LinuxKit configs may live in .core/linuxkit/*.yml or *.yaml.
- result.Markers[markerLinuxKitNestedYML] = hasYAMLInDir(fs, ax.Join(dir, ".core", "linuxkit"))
- result.Markers[markerLinuxKitNestedYAML] = result.Markers[markerLinuxKitNestedYML]
-
- // Subtree npm detection
- result.HasSubtreeNpm = HasSubtreeNpm(fs, dir)
- result.HasSubtreePackageJSON = result.HasSubtreeNpm
- result.HasSubtreeDenoManifest = hasSubtreeDenoManifest(fs, dir)
- result.HasPackageJSON = result.HasRootPackageJSON || result.HasFrontendPackageJSON || result.HasSubtreeNpm
- result.HasDenoManifest = result.Markers[markerDenoJSON] ||
- result.Markers[markerDenoJSONC] ||
- result.Markers[markerFrontendDenoJSON] ||
- result.Markers[markerFrontendDenoJSONC] ||
- result.HasSubtreeDenoManifest
-
- // Frontend detection: root manifests, frontend/ manifests, or nested frontend trees.
- result.HasFrontend = result.HasPackageJSON || result.HasDenoManifest
- result.HasGoToolchain = result.HasRootGoMod || result.HasRootGoWork || hasNestedGoToolchain(fs, dir, 0)
-
- result.Types = types
- if configuredType, ok := configuredProjectType(fs, dir); ok {
- result.ConfiguredType = string(configuredType)
- result.ConfiguredBuildType = result.ConfiguredType
- }
-
- // Linux distro detection: used for distro-sensitive build flags.
- result.Distro = detectDistroVersion(fs)
- result.LinuxPackages = ResolveLinuxPackages(result.Types, result.Distro)
- result.WebKitPackage = firstString(result.LinuxPackages)
- if git := DetectGitHubMetadata(); git != nil {
- result.Ref = git.Ref
- result.Branch = git.Branch
- result.Tag = git.Tag
- result.IsTag = git.IsTag
- result.SHA = git.SHA
- result.ShortSHA = git.ShortSHA
- result.Repo = git.Repo
- result.Owner = git.Owner
- } else if git := detectLocalGitMetadata(dir); git != nil {
- result.Ref = git.Ref
- result.Branch = git.Branch
- result.Tag = git.Tag
- result.IsTag = git.IsTag
- result.SHA = git.SHA
- result.ShortSHA = git.ShortSHA
- result.Repo = git.Repo
- result.Owner = git.Owner
- }
-
- // Primary stack: first detected type as string, or empty
- if len(types) > 0 {
- result.PrimaryStack = string(types[0])
- }
- result.SuggestedStack = SuggestStack(types)
- result.PrimaryStackSuggestion = resolvePrimaryStackSuggestion(result)
-
- return core.Ok(result)
-}
-
-func discoverHostOS() string {
- if goos := core.Env("GOOS"); goos != "" {
- return goos
- }
- return runtime.GOOS
-}
-
-func discoverHostArch() string {
- if goarch := core.Env("GOARCH"); goarch != "" {
- return goarch
- }
-
- if hosttype := core.Env("HOSTTYPE"); hosttype != "" {
- switch hosttype {
- case "x86_64", "amd64":
- return "amd64"
- case "x86", "i386", "i686":
- return "386"
- case "aarch64", "arm64":
- return "arm64"
- case "arm", "armv7l", "armv6l":
- return "arm"
- case "riscv64":
- return "riscv64"
- }
-
- return hosttype
- }
-
- return runtime.GOARCH
-}
-
-// SuggestStack returns the action-oriented stack suggestion for the detected
-// project markers. This keeps discovery compatible with the v3 action naming,
-// where Wails-backed projects use the "wails2" stack identifier.
-//
-// stack := build.SuggestStack([]build.ProjectType{build.ProjectTypeWails}) // "wails2"
-func SuggestStack(types []ProjectType) string {
- if len(types) == 0 {
- return "unknown"
- }
-
- switch types[0] {
- case ProjectTypeWails:
- return "wails2"
- case ProjectTypeCPP:
- return "cpp"
- case ProjectTypeDocs:
- return "docs"
- case ProjectTypeNode:
- return "node"
- default:
- return string(types[0])
- }
-}
-
-func configuredProjectType(fs storage.Medium, dir string) (ProjectType, bool) {
- if fs == nil || !ConfigExists(fs, dir) {
- return "", false
- }
-
- cfgResult := LoadConfig(fs, dir)
- if !cfgResult.OK || cfgResult.Value == nil {
- return "", false
- }
- cfg := cfgResult.Value.(*BuildConfig)
-
- projectType, ok := parseProjectType(cfg.Build.Type)
- if !ok {
- return "", false
- }
-
- return projectType, true
-}
-
-func parseProjectType(value string) (ProjectType, bool) {
- projectType := ProjectType(core.Lower(core.Trim(value)))
-
- switch projectType {
- case ProjectTypeGo,
- ProjectTypeWails,
- ProjectTypeNode,
- ProjectTypePHP,
- ProjectTypeCPP,
- ProjectTypeDocker,
- ProjectTypeLinuxKit,
- ProjectTypeTaskfile,
- ProjectTypeDocs,
- ProjectTypePython,
- ProjectTypeRust:
- return projectType, true
- default:
- return "", false
- }
-}
-
-// ResolveLinuxPackages returns distro-aware system dependencies for the detected stack.
-//
-// packages := build.ResolveLinuxPackages([]build.ProjectType{build.ProjectTypeWails}, "24.04")
-// // []string{"libwebkit2gtk-4.1-dev"}
-func ResolveLinuxPackages(types []ProjectType, distro string) []string {
- if len(types) == 0 || distro == "" {
- return nil
- }
-
- var packages []string
- if containsProjectType(types, ProjectTypeWails) {
- if isUbuntu2404OrNewer(distro) {
- packages = append(packages, "libwebkit2gtk-4.1-dev")
- } else {
- packages = append(packages, "libwebkit2gtk-4.0-dev")
- }
- }
-
- return deduplicateStrings(packages)
-}
-
-func containsProjectType(types []ProjectType, projectType ProjectType) bool {
- for _, candidate := range types {
- if candidate == projectType {
- return true
- }
- }
- return false
-}
-
-// hasFrontendManifest reports whether a frontend directory contains a supported manifest.
-func hasFrontendManifest(fs storage.Medium, dir string) bool {
- if fs == nil {
- return false
- }
- return fs.IsFile(ax.Join(dir, markerNodePackage)) ||
- fs.IsFile(ax.Join(dir, "deno.json")) ||
- fs.IsFile(ax.Join(dir, "deno.jsonc"))
-}
-
-// hasSubtreeFrontendManifest checks for package.json or deno.json within depth 2 subdirectories.
-func hasSubtreeFrontendManifest(fs storage.Medium, dir string) bool {
- if fs == nil {
- return false
- }
- entriesResult := fs.List(dir)
- if !entriesResult.OK {
- return false
- }
-
- for _, entry := range entriesResult.Value.([]core.FsDirEntry) {
- if !entry.IsDir() {
- continue
- }
- name := entry.Name()
- if shouldSkipSubtreeDir(name) || name == "frontend" {
- continue
- }
-
- subdir := ax.Join(dir, name)
- if hasFrontendManifest(fs, subdir) {
- return true
- }
-
- subEntriesResult := fs.List(subdir)
- if !subEntriesResult.OK {
- continue
- }
- for _, subEntry := range subEntriesResult.Value.([]core.FsDirEntry) {
- if !subEntry.IsDir() {
- continue
- }
- if shouldSkipSubtreeDir(subEntry.Name()) {
- continue
- }
- nested := ax.Join(subdir, subEntry.Name())
- if hasFrontendManifest(fs, nested) {
- return true
- }
- }
- }
-
- return false
-}
-
-func hasSubtreeDenoManifest(fs storage.Medium, dir string) bool {
- return hasSubtreeManifest(fs, dir, 0, func(fs storage.Medium, candidate string) bool {
- if fs == nil {
- return false
- }
- return fs.IsFile(ax.Join(candidate, markerDenoJSON)) || fs.IsFile(ax.Join(candidate, markerDenoJSONC))
- })
-}
-
-func findMkDocsConfigInSubtree(fs storage.Medium, dir string, depth int) string {
- if fs == nil {
- return ""
- }
- if depth >= 2 {
- return ""
- }
-
- entriesResult := fs.List(dir)
- if !entriesResult.OK {
- return ""
- }
-
- for _, entry := range entriesResult.Value.([]core.FsDirEntry) {
- if !entry.IsDir() {
- continue
- }
-
- name := entry.Name()
- if shouldSkipSubtreeDir(name) {
- continue
- }
-
- candidateDir := ax.Join(dir, name)
- for _, marker := range []string{markerMkDocs, markerMkDocsYAML} {
- if fileExists(fs, ax.Join(candidateDir, marker)) {
- return ax.Join(candidateDir, marker)
- }
- }
-
- if nested := findMkDocsConfigInSubtree(fs, candidateDir, depth+1); nested != "" {
- return nested
- }
- }
-
- return ""
-}
-
-func hasNestedGoToolchain(fs storage.Medium, dir string, depth int) bool {
- return hasSubtreeManifest(fs, dir, depth, func(fs storage.Medium, candidate string) bool {
- if fs == nil {
- return false
- }
- return fs.IsFile(ax.Join(candidate, markerGoMod)) || fs.IsFile(ax.Join(candidate, markerGoWork))
- }, 4)
-}
-
-func hasSubtreeManifest(fs storage.Medium, dir string, depth int, match func(storage.Medium, string) bool, maxDepth ...int) bool {
- if fs == nil || match == nil {
- return false
- }
- limit := 2
- if len(maxDepth) > 0 {
- limit = maxDepth[0]
- }
- if depth >= limit {
- return false
- }
-
- entriesResult := fs.List(dir)
- if !entriesResult.OK {
- return false
- }
-
- for _, entry := range entriesResult.Value.([]core.FsDirEntry) {
- if !entry.IsDir() {
- continue
- }
-
- name := entry.Name()
- if shouldSkipSubtreeDir(name) || name == "frontend" {
- continue
- }
-
- candidateDir := ax.Join(dir, name)
- if match(fs, candidateDir) {
- return true
- }
-
- if hasSubtreeManifest(fs, candidateDir, depth+1, match, limit) {
- return true
- }
- }
-
- return false
-}
-
-func resolvePrimaryStackSuggestion(result *DiscoveryResult) string {
- if result == nil {
- return "unknown"
- }
- if result.ConfiguredType != "" {
- return SuggestStack([]ProjectType{ProjectType(result.ConfiguredType)})
- }
-
- switch {
- case result.HasRootWailsJSON:
- return "wails2"
- case (result.HasRootGoMod || result.HasRootGoWork) && result.HasFrontend:
- return "wails2"
- case result.HasRootCMakeLists:
- return "cpp"
- case result.HasDocsConfig && !result.HasGoToolchain:
- return "docs"
- case result.HasFrontend && !result.HasGoToolchain:
- return "node"
- case result.HasGoToolchain:
- return "go"
- case result.HasDocsConfig:
- return "docs"
- case result.HasFrontend:
- return "node"
- default:
- return "unknown"
- }
-}
-
-func firstString(values []string) string {
- if len(values) == 0 {
- return ""
- }
- return values[0]
-}
-
-// hasGoRootMarker reports whether the project root contains a Go module or workspace marker.
-func hasGoRootMarker(fs storage.Medium, dir string) bool {
- return fileExists(fs, ax.Join(dir, markerGoMod)) ||
- fileExists(fs, ax.Join(dir, markerGoWork))
-}
-
-// fileExists checks if a file exists and is not a directory.
-func fileExists(fs storage.Medium, path string) bool {
- if fs == nil {
- return false
- }
- return fs.IsFile(path)
-}
-
-func collectMarkerPresence(fs storage.Medium, dir string, paths []string) map[string]bool {
- markers := make(map[string]bool, len(paths))
- for _, path := range paths {
- markers[path] = fileExists(fs, ax.Join(dir, path))
- }
- return markers
-}
-
-func shouldSkipSubtreeDir(name string) bool {
- return name == "node_modules" || core.HasPrefix(name, ".")
-}
-
-// ResolveDockerfilePath returns the first Docker manifest path that exists.
-//
-// dockerfile := build.ResolveDockerfilePath(storage.Local, ".")
-func ResolveDockerfilePath(fs storage.Medium, dir string) string {
- for _, path := range []string{
- ax.Join(dir, "Dockerfile"),
- ax.Join(dir, "Containerfile"),
- ax.Join(dir, "dockerfile"),
- ax.Join(dir, "containerfile"),
- } {
- if fileExists(fs, path) {
- return path
- }
- }
- return ""
-}
-
-// IsDockerProject checks if the directory contains a Dockerfile or Containerfile.
-//
-// if build.IsDockerProject(storage.Local, ".") { ... }
-func IsDockerProject(fs storage.Medium, dir string) bool {
- return ResolveDockerfilePath(fs, dir) != ""
-}
-
-// IsLinuxKitProject checks for linuxkit.yml or .core/linuxkit/*.yml.
-//
-// ok := build.IsLinuxKitProject(storage.Local, ".")
-func IsLinuxKitProject(fs storage.Medium, dir string) bool {
- if fileExists(fs, ax.Join(dir, markerLinuxKitYAML)) ||
- fileExists(fs, ax.Join(dir, markerLinuxKitYAMLAlt)) {
- return true
- }
- return hasYAMLInDir(fs, ax.Join(dir, ".core", "linuxkit"))
-}
-
-// IsTaskfileProject checks for supported Taskfile names in the project root.
-//
-// ok := build.IsTaskfileProject(storage.Local, ".")
-func IsTaskfileProject(fs storage.Medium, dir string) bool {
- for _, name := range []string{
- markerTaskfileYML,
- markerTaskfileYAML,
- markerTaskfileBare,
- markerTaskfileLowerYML,
- markerTaskfileLowerYAML,
- } {
- if fileExists(fs, ax.Join(dir, name)) {
- return true
- }
- }
- return false
-}
-
-// hasYAMLInDir reports whether a directory contains at least one YAML file.
-func hasYAMLInDir(fs storage.Medium, dir string) bool {
- if fs == nil {
- return false
- }
- if !fs.IsDir(dir) {
- return false
- }
-
- entriesResult := fs.List(dir)
- if !entriesResult.OK {
- return false
- }
-
- for _, entry := range entriesResult.Value.([]core.FsDirEntry) {
- if entry.IsDir() {
- continue
- }
- name := core.Lower(entry.Name())
- if core.HasSuffix(name, ".yml") || core.HasSuffix(name, ".yaml") {
- return true
- }
- }
-
- return false
-}
-
-// detectDistroVersion extracts the Ubuntu VERSION_ID from os-release data.
-func detectDistroVersion(fs storage.Medium) string {
- if fs == nil {
- return ""
- }
-
- for _, path := range []string{"/etc/os-release", "/usr/lib/os-release"} {
- content := fs.Read(path)
- if !content.OK {
- continue
- }
-
- if distro := parseOSReleaseDistro(content.Value.(string)); distro != "" {
- return distro
- }
- }
-
- return ""
-}
-
-// parseOSReleaseDistro returns VERSION_ID for Ubuntu-style os-release content.
-func parseOSReleaseDistro(content string) string {
- var id string
- var idLike string
- var version string
-
- for _, line := range core.Split(content, "\n") {
- line = core.Trim(line)
- if line == "" || core.HasPrefix(line, "#") {
- continue
- }
-
- parts := core.SplitN(line, "=", 2)
- if len(parts) != 2 {
- continue
- }
-
- key := core.Trim(parts[0])
- value := core.Trim(parts[1])
- value = core.TrimPrefix(value, `"`)
- value = core.TrimSuffix(value, `"`)
- value = core.TrimPrefix(value, `'`)
- value = core.TrimSuffix(value, `'`)
-
- switch key {
- case "ID":
- id = value
- case "ID_LIKE":
- idLike = value
- case "VERSION_ID":
- version = value
- }
- }
-
- if version == "" {
- return ""
- }
-
- if id == "ubuntu" || core.Contains(" "+idLike+" ", " ubuntu ") {
- return version
- }
-
- return ""
-}
diff --git a/pkg/build/discovery_example_test.go b/pkg/build/discovery_example_test.go
deleted file mode 100644
index 97d1959..0000000
--- a/pkg/build/discovery_example_test.go
+++ /dev/null
@@ -1,143 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleDiscover references Discover on this package API surface.
-func ExampleDiscover() {
- _ = Discover
- core.Println("Discover")
- // Output: Discover
-}
-
-// ExamplePrimaryType references PrimaryType on this package API surface.
-func ExamplePrimaryType() {
- _ = PrimaryType
- core.Println("PrimaryType")
- // Output: PrimaryType
-}
-
-// ExampleIsGoProject references IsGoProject on this package API surface.
-func ExampleIsGoProject() {
- _ = IsGoProject
- core.Println("IsGoProject")
- // Output: IsGoProject
-}
-
-// ExampleIsWailsProject references IsWailsProject on this package API surface.
-func ExampleIsWailsProject() {
- _ = IsWailsProject
- core.Println("IsWailsProject")
- // Output: IsWailsProject
-}
-
-// ExampleIsNodeProject references IsNodeProject on this package API surface.
-func ExampleIsNodeProject() {
- _ = IsNodeProject
- core.Println("IsNodeProject")
- // Output: IsNodeProject
-}
-
-// ExampleIsPHPProject references IsPHPProject on this package API surface.
-func ExampleIsPHPProject() {
- _ = IsPHPProject
- core.Println("IsPHPProject")
- // Output: IsPHPProject
-}
-
-// ExampleIsCPPProject references IsCPPProject on this package API surface.
-func ExampleIsCPPProject() {
- _ = IsCPPProject
- core.Println("IsCPPProject")
- // Output: IsCPPProject
-}
-
-// ExampleIsMkDocsProject references IsMkDocsProject on this package API surface.
-func ExampleIsMkDocsProject() {
- _ = IsMkDocsProject
- core.Println("IsMkDocsProject")
- // Output: IsMkDocsProject
-}
-
-// ExampleIsDocsProject references IsDocsProject on this package API surface.
-func ExampleIsDocsProject() {
- _ = IsDocsProject
- core.Println("IsDocsProject")
- // Output: IsDocsProject
-}
-
-// ExampleResolveMkDocsConfigPath references ResolveMkDocsConfigPath on this package API surface.
-func ExampleResolveMkDocsConfigPath() {
- _ = ResolveMkDocsConfigPath
- core.Println("ResolveMkDocsConfigPath")
- // Output: ResolveMkDocsConfigPath
-}
-
-// ExampleHasSubtreeNpm references HasSubtreeNpm on this package API surface.
-func ExampleHasSubtreeNpm() {
- _ = HasSubtreeNpm
- core.Println("HasSubtreeNpm")
- // Output: HasSubtreeNpm
-}
-
-// ExampleIsPythonProject references IsPythonProject on this package API surface.
-func ExampleIsPythonProject() {
- _ = IsPythonProject
- core.Println("IsPythonProject")
- // Output: IsPythonProject
-}
-
-// ExampleIsRustProject references IsRustProject on this package API surface.
-func ExampleIsRustProject() {
- _ = IsRustProject
- core.Println("IsRustProject")
- // Output: IsRustProject
-}
-
-// ExampleDiscoverFull references DiscoverFull on this package API surface.
-func ExampleDiscoverFull() {
- _ = DiscoverFull
- core.Println("DiscoverFull")
- // Output: DiscoverFull
-}
-
-// ExampleSuggestStack references SuggestStack on this package API surface.
-func ExampleSuggestStack() {
- _ = SuggestStack
- core.Println("SuggestStack")
- // Output: SuggestStack
-}
-
-// ExampleResolveLinuxPackages references ResolveLinuxPackages on this package API surface.
-func ExampleResolveLinuxPackages() {
- _ = ResolveLinuxPackages
- core.Println("ResolveLinuxPackages")
- // Output: ResolveLinuxPackages
-}
-
-// ExampleResolveDockerfilePath references ResolveDockerfilePath on this package API surface.
-func ExampleResolveDockerfilePath() {
- _ = ResolveDockerfilePath
- core.Println("ResolveDockerfilePath")
- // Output: ResolveDockerfilePath
-}
-
-// ExampleIsDockerProject references IsDockerProject on this package API surface.
-func ExampleIsDockerProject() {
- _ = IsDockerProject
- core.Println("IsDockerProject")
- // Output: IsDockerProject
-}
-
-// ExampleIsLinuxKitProject references IsLinuxKitProject on this package API surface.
-func ExampleIsLinuxKitProject() {
- _ = IsLinuxKitProject
- core.Println("IsLinuxKitProject")
- // Output: IsLinuxKitProject
-}
-
-// ExampleIsTaskfileProject references IsTaskfileProject on this package API surface.
-func ExampleIsTaskfileProject() {
- _ = IsTaskfileProject
- core.Println("IsTaskfileProject")
- // Output: IsTaskfileProject
-}
diff --git a/pkg/build/discovery_test.go b/pkg/build/discovery_test.go
deleted file mode 100644
index 18287d6..0000000
--- a/pkg/build/discovery_test.go
+++ /dev/null
@@ -1,2362 +0,0 @@
-package build
-
-import (
- "runtime"
- "testing"
-
- "dappco.re/go/build/internal/ax"
-
- core "dappco.re/go"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// setupTestDir creates a temporary directory with the specified marker files.
-func setupTestDir(t *testing.T, markers ...string) string {
- t.Helper()
- dir := t.TempDir()
- for _, m := range markers {
- path := ax.Join(dir, m)
- requireDiscoveryOKResult(t, ax.WriteFile(path, []byte("{}"), 0644))
-
- }
- return dir
-}
-
-func requireDiscoveryOKResult(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requireDiscoveryTypes(t *testing.T, result core.Result) []ProjectType {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]ProjectType)
-}
-
-func requireDiscoveryPrimary(t *testing.T, result core.Result) ProjectType {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(ProjectType)
-}
-
-func requireDiscoveryFull(t *testing.T, result core.Result) *DiscoveryResult {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(*DiscoveryResult)
-}
-
-func requireDiscoveryString(t *testing.T, result core.Result) string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func setupDiscoveryFile(t *testing.T, relPath string, content string) string {
- t.Helper()
- dir := t.TempDir()
- writeDiscoveryFile(t, dir, relPath, content)
- return dir
-}
-
-func writeDiscoveryFile(t *testing.T, dir string, relPath string, content string) {
- t.Helper()
- path := ax.Join(dir, relPath)
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Dir(path), 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(path, []byte(content), 0o644))
-}
-
-func assertDiscoverTypes(t *testing.T, fs storage.Medium, dir string, want []ProjectType) {
- t.Helper()
-
- types := requireDiscoveryTypes(t, Discover(fs, dir))
- if !stdlibAssertEqual(want, types) {
- t.Fatalf("want %v, got %v", want, types)
- }
-}
-
-func assertDiscoverEmpty(t *testing.T, fs storage.Medium, dir string) {
- t.Helper()
-
- types := requireDiscoveryTypes(t, Discover(fs, dir))
- if !stdlibAssertEmpty(types) {
- t.Fatalf("expected empty, got %v", types)
- }
-}
-
-func assertDiscoverFullStack(t *testing.T, fs storage.Medium, dir string, want []ProjectType, wantStack string, markers ...string) *DiscoveryResult {
- t.Helper()
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual(want, result.Types) {
- t.Fatalf("want %v, got %v", want, result.Types)
- }
- if !stdlibAssertEqual(wantStack, result.PrimaryStack) {
- t.Fatalf("want %v, got %v", wantStack, result.PrimaryStack)
- }
- for _, marker := range markers {
- if !result.Markers[marker] {
- t.Fatalf("expected marker %q", marker)
- }
- }
- return result
-}
-
-func TestDiscovery_Discover_Good(t *testing.T) {
- fs := storage.Local
- _ = requireDiscoveryTypes(t, Discover(fs, setupTestDir(t, "go.mod")))
-
- t.Run("prefers configured build type from .core/build.yaml", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, ".core"), 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, ".core", "build.yaml"), []byte("build:\n type: docker\n"), 0o644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeDocker})
-
- })
-
- t.Run("configured build type short-circuits marker detection", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, ".core"), 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, ".core", "build.yaml"), []byte("build:\n type: docker\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeDocker})
-
- })
-
- t.Run("detects Go project", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeGo})
-
- })
-
- t.Run("detects Go workspace project", func(t *testing.T) {
- dir := setupTestDir(t, "go.work")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeGo})
-
- })
-
- t.Run("detects Wails project with priority over Go", func(t *testing.T) {
- dir := setupTestDir(t, "wails.json", "go.mod")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeWails, ProjectTypeGo})
-
- })
-
- t.Run("detects Node.js project", func(t *testing.T) {
- dir := setupTestDir(t, "package.json")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeNode})
-
- })
-
- t.Run("detects Deno project", func(t *testing.T) {
- dir := setupTestDir(t, "deno.json")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeNode})
-
- })
-
- t.Run("detects nested Node.js project", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "apps", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "package.json"), []byte("{}"), 0644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeNode})
-
- })
-
- t.Run("detects nested Deno project", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "apps", "site")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "deno.jsonc"), []byte("{}"), 0o644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeNode})
-
- })
-
- t.Run("detects Wails project from go.mod and root package.json", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod", "package.json")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode})
-
- })
-
- t.Run("detects Wails project from go.mod and nested frontend package.json", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example"), 0o644))
-
- nested := ax.Join(dir, "apps", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "package.json"), []byte("{}"), 0o644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode})
-
- })
-
- t.Run("detects Wails project from go.work and frontend deno.json", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.work"), []byte("go 1.26\nuse ."), 0o644))
-
- frontend := ax.Join(dir, "frontend")
- requireDiscoveryOKResult(t, ax.MkdirAll(frontend, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontend, "deno.json"), []byte("{}"), 0o644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode})
-
- })
-
- t.Run("detects Wails project from go.mod and nested frontend deno.jsonc", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example"), 0o644))
-
- nested := ax.Join(dir, "apps", "site")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "deno.jsonc"), []byte("{}"), 0o644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode})
-
- })
-
- t.Run("detects PHP project", func(t *testing.T) {
- dir := setupTestDir(t, "composer.json")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypePHP})
-
- })
-
- t.Run("detects docs project", func(t *testing.T) {
- dir := setupTestDir(t, "mkdocs.yml")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeDocs})
-
- })
-
- t.Run("keeps docs after generic Node markers", func(t *testing.T) {
- dir := setupTestDir(t, "mkdocs.yml", "package.json")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeNode, ProjectTypeDocs})
-
- })
-
- t.Run("detects docs project with mkdocs.yaml", func(t *testing.T) {
- dir := setupTestDir(t, "mkdocs.yaml")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeDocs})
-
- })
-
- t.Run("detects docs project in docs directory", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, "docs"), 0755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "docs", "mkdocs.yml"), []byte("site_name: Demo\n"), 0644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeDocs})
-
- })
-
- t.Run("detects docs project in docs directory with mkdocs.yaml", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, "docs"), 0755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "docs", "mkdocs.yaml"), []byte("site_name: Demo\n"), 0644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeDocs})
-
- })
-
- t.Run("detects Python project with pyproject.toml", func(t *testing.T) {
- dir := setupTestDir(t, "pyproject.toml")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypePython})
-
- })
-
- t.Run("detects Python project with requirements.txt", func(t *testing.T) {
- dir := setupTestDir(t, "requirements.txt")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypePython})
-
- })
-
- t.Run("detects Python only once with both markers", func(t *testing.T) {
- dir := setupTestDir(t, "pyproject.toml", "requirements.txt")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypePython})
-
- })
-
- t.Run("detects Rust project", func(t *testing.T) {
- dir := setupTestDir(t, "Cargo.toml")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeRust})
-
- })
-
- t.Run("detects Docker project", func(t *testing.T) {
- dir := setupTestDir(t, "Dockerfile")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeDocker})
-
- })
-
- t.Run("detects Containerfile project", func(t *testing.T) {
- dir := setupTestDir(t, "Containerfile")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeDocker})
-
- })
-
- t.Run("detects LinuxKit project", func(t *testing.T) {
- dir := t.TempDir()
- lkDir := ax.Join(dir, ".core", "linuxkit")
- requireDiscoveryOKResult(t, ax.MkdirAll(lkDir, 0755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(lkDir, "server.yml"), []byte("kernel:\n"), 0644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeLinuxKit})
-
- })
-
- t.Run("detects LinuxKit project from yaml config", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "linuxkit.yaml"), []byte("kernel:\n"), 0644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeLinuxKit})
-
- })
-
- t.Run("detects C++ project", func(t *testing.T) {
- dir := setupTestDir(t, "CMakeLists.txt")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeCPP})
-
- })
-
- t.Run("detects Taskfile project", func(t *testing.T) {
- dir := setupTestDir(t, "Taskfile.yml")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeTaskfile})
-
- })
-
- t.Run("detects multiple project types", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod", "package.json")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode})
-
- })
-
- t.Run("preserves priority when core and fallback markers overlap", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod", "Dockerfile", "Taskfile.yml")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeGo, ProjectTypeDocker, ProjectTypeTaskfile})
-
- })
-
- t.Run("prefers C++ ahead of Docker and Taskfile in fallback detection", func(t *testing.T) {
- dir := setupTestDir(t, "CMakeLists.txt", "Dockerfile", "Taskfile.yml")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeCPP, ProjectTypeDocker, ProjectTypeTaskfile})
-
- })
-
- t.Run("keeps docs after taskfile and docker per RFC priority", func(t *testing.T) {
- dir := setupTestDir(t, "mkdocs.yml", "Dockerfile", "Taskfile.yml")
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeDocker, ProjectTypeTaskfile, ProjectTypeDocs})
-
- })
-
- t.Run("empty directory returns empty slice", func(t *testing.T) {
- dir := t.TempDir()
- assertDiscoverEmpty(t, fs, dir)
-
- })
-}
-
-func TestDiscovery_Discover_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("non-existent directory returns empty slice", func(t *testing.T) {
- types := requireDiscoveryTypes(t, Discover(fs, "/non/existent/path"))
- if !stdlibAssertEmpty(types) {
- t.Fatalf("expected empty, got %v", types)
- }
-
- })
-
- t.Run("directory marker is ignored", func(t *testing.T) {
- dir := t.TempDir()
- // Create go.mod as a directory instead of a file
- requireDiscoveryOKResult(t, ax.Mkdir(ax.Join(dir, "go.mod"), 0755))
-
- assertDiscoverEmpty(t, fs, dir)
-
- })
-
- t.Run("unsupported configured build type is ignored", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, ".core"), 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, ".core", "build.yaml"), []byte("build:\n type: kotlin\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
-
- assertDiscoverTypes(t, fs, dir, []ProjectType{ProjectTypeGo})
-
- })
-}
-
-func TestDiscovery_PrimaryType_Good(t *testing.T) {
- fs := storage.Local
- t.Run("returns configured build type from .core/build.yaml", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, ".core"), 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, ".core", "build.yaml"), []byte("build:\n type: taskfile\n"), 0o644))
-
- primary := requireDiscoveryPrimary(t, PrimaryType(fs, dir))
- if !stdlibAssertEqual(ProjectTypeTaskfile, primary) {
- t.Fatalf("want %v, got %v", ProjectTypeTaskfile, primary)
- }
-
- })
-
- t.Run("returns configured type when markers disagree", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, ".core"), 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, ".core", "build.yaml"), []byte("build:\n type: taskfile\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644))
-
- primary := requireDiscoveryPrimary(t, PrimaryType(fs, dir))
- if !stdlibAssertEqual(ProjectTypeTaskfile, primary) {
- t.Fatalf("want %v, got %v", ProjectTypeTaskfile, primary)
- }
-
- })
-
- t.Run("returns wails for wails project", func(t *testing.T) {
- dir := setupTestDir(t, "wails.json", "go.mod")
- primary := requireDiscoveryPrimary(t, PrimaryType(fs, dir))
- if !stdlibAssertEqual(ProjectTypeWails, primary) {
- t.Fatalf("want %v, got %v", ProjectTypeWails, primary)
- }
-
- })
-
- t.Run("returns go for go-only project", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod")
- primary := requireDiscoveryPrimary(t, PrimaryType(fs, dir))
- if !stdlibAssertEqual(ProjectTypeGo, primary) {
- t.Fatalf("want %v, got %v", ProjectTypeGo, primary)
- }
-
- })
-
- t.Run("returns node for nested package.json project", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "apps", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "package.json"), []byte("{}"), 0644))
-
- primary := requireDiscoveryPrimary(t, PrimaryType(fs, dir))
- if !stdlibAssertEqual(ProjectTypeNode, primary) {
- t.Fatalf("want %v, got %v", ProjectTypeNode, primary)
- }
-
- })
-
- t.Run("returns node for root deno project", func(t *testing.T) {
- dir := setupTestDir(t, "deno.jsonc")
- primary := requireDiscoveryPrimary(t, PrimaryType(fs, dir))
- if !stdlibAssertEqual(ProjectTypeNode, primary) {
- t.Fatalf("want %v, got %v", ProjectTypeNode, primary)
- }
-
- })
-
- t.Run("returns node when mkdocs and package.json coexist", func(t *testing.T) {
- dir := setupTestDir(t, "mkdocs.yml", "package.json")
- primary := requireDiscoveryPrimary(t, PrimaryType(fs, dir))
- if !stdlibAssertEqual(ProjectTypeNode, primary) {
- t.Fatalf("want %v, got %v", ProjectTypeNode, primary)
- }
-
- })
-
- t.Run("returns wails for go.mod with nested frontend package.json", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example"), 0o644))
-
- nested := ax.Join(dir, "apps", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "package.json"), []byte("{}"), 0o644))
-
- primary := requireDiscoveryPrimary(t, PrimaryType(fs, dir))
- if !stdlibAssertEqual(ProjectTypeWails, primary) {
- t.Fatalf("want %v, got %v", ProjectTypeWails, primary)
- }
-
- })
-
- t.Run("returns empty string for empty directory", func(t *testing.T) {
- dir := t.TempDir()
- primary := requireDiscoveryPrimary(t, PrimaryType(fs, dir))
- if !stdlibAssertEmpty(primary) {
- t.Fatalf("expected empty, got %v", primary)
- }
-
- })
-}
-
-func TestDiscovery_IsGoProject_Good(t *testing.T) {
- fs := storage.Local
- t.Run("true with go.mod", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod")
- if !(IsGoProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with go.work", func(t *testing.T) {
- dir := setupTestDir(t, "go.work")
- if !(IsGoProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with wails.json", func(t *testing.T) {
- dir := setupTestDir(t, "wails.json")
- if !(IsGoProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("false without markers", func(t *testing.T) {
- dir := t.TempDir()
- if IsGoProject(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsWailsProject_Good(t *testing.T) {
- fs := storage.Local
- t.Run("true with wails.json", func(t *testing.T) {
- dir := setupTestDir(t, "wails.json")
- if !(IsWailsProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with go.mod and root package.json", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod", "package.json")
- if !(IsWailsProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with go.mod and nested frontend package.json", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example"), 0o644))
-
- nested := ax.Join(dir, "apps", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "package.json"), []byte("{}"), 0o644))
- if !(IsWailsProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with go.work and frontend deno.json", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.work"), []byte("go 1.26\nuse ."), 0o644))
-
- frontend := ax.Join(dir, "frontend")
- requireDiscoveryOKResult(t, ax.MkdirAll(frontend, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontend, "deno.json"), []byte("{}"), 0o644))
- if !(IsWailsProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("false with only go.mod", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod")
- if IsWailsProject(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsNodeProject_Good(t *testing.T) {
- fs := storage.Local
-
- t.Run("true with package.json", func(t *testing.T) {
- dir := setupTestDir(t, "package.json")
- if !(IsNodeProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with deno.json", func(t *testing.T) {
- dir := setupTestDir(t, "deno.json")
- if !(IsNodeProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with deno.jsonc", func(t *testing.T) {
- dir := setupTestDir(t, "deno.jsonc")
- if !(IsNodeProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with frontend package.json", func(t *testing.T) {
- dir := t.TempDir()
- frontend := ax.Join(dir, "frontend")
- requireDiscoveryOKResult(t, ax.MkdirAll(frontend, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontend, "package.json"), []byte("{}"), 0o644))
- if !(IsNodeProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with nested package.json", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "apps", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "package.json"), []byte("{}"), 0o644))
- if !(IsNodeProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with nested deno.json", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "apps", "docs")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "deno.json"), []byte("{}"), 0o644))
- if !(IsNodeProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("false without markers", func(t *testing.T) {
- if IsNodeProject(fs, t.TempDir()) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsPHPProject_Good(t *testing.T) {
- fs := storage.Local
- t.Run("true with composer.json", func(t *testing.T) {
- dir := setupTestDir(t, "composer.json")
- if !(IsPHPProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("false without composer.json", func(t *testing.T) {
- dir := t.TempDir()
- if IsPHPProject(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_Target_Good(t *testing.T) {
- target := Target{OS: "linux", Arch: "amd64"}
- if !stdlibAssertEqual("linux/amd64", target.String()) {
- t.Fatalf("want %v, got %v", "linux/amd64", target.String())
- }
-
-}
-
-func TestDiscovery_FileExistsGood(t *testing.T) {
- fs := storage.Local
- t.Run("returns true for existing file", func(t *testing.T) {
- dir := t.TempDir()
- path := ax.Join(dir, "test.txt")
- requireDiscoveryOKResult(t, ax.WriteFile(path, []byte("content"), 0644))
- if !(fileExists(fs, path)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns false for directory", func(t *testing.T) {
- dir := t.TempDir()
- if fileExists(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("returns false for non-existent path", func(t *testing.T) {
- if fileExists(fs, "/non/existent/file") {
- t.Fatal("expected false")
- }
-
- })
-}
-
-// TestDiscover_Testdata tests discovery using the testdata fixtures.
-// These serve as integration tests with realistic project structures.
-func TestDiscovery_DiscoverTestdataGood(t *testing.T) {
- fs := storage.Local
- testdataDir := requireDiscoveryString(t, ax.Abs("testdata"))
-
- tests := []struct {
- name string
- dir string
- expected []ProjectType
- }{
- {"go-project", "go-project", []ProjectType{ProjectTypeGo}},
- {"wails-project", "wails-project", []ProjectType{ProjectTypeWails, ProjectTypeGo}},
- {"node-project", "node-project", []ProjectType{ProjectTypeNode}},
- {"php-project", "php-project", []ProjectType{ProjectTypePHP}},
- {"multi-project", "multi-project", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}},
- {"empty-project", "empty-project", []ProjectType{}},
- {"docs-project", "docs-project", []ProjectType{ProjectTypeDocs}},
- {"python-project", "python-project", []ProjectType{ProjectTypePython}},
- {"rust-project", "rust-project", []ProjectType{ProjectTypeRust}},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- dir := ax.Join(testdataDir, tt.dir)
- types := requireDiscoveryTypes(t, Discover(fs, dir))
-
- if len(tt.expected) == 0 {
- if !stdlibAssertEmpty(types) {
- t.Fatalf("expected empty, got %v", types)
- }
-
- } else {
- if !stdlibAssertEqual(tt.expected, types) {
- t.Fatalf("want %v, got %v", tt.expected, types)
- }
-
- }
- })
- }
-}
-
-func TestDiscovery_IsMkDocsProject_Good(t *testing.T) {
- fs := storage.Local
- t.Run("true with mkdocs.yml", func(t *testing.T) {
- dir := setupTestDir(t, "mkdocs.yml")
- if !(IsMkDocsProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with mkdocs.yaml", func(t *testing.T) {
- dir := setupTestDir(t, "mkdocs.yaml")
- if !(IsMkDocsProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with nested mkdocs.yml", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "docs", "guide")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "mkdocs.yml"), []byte("site_name: Guide"), 0o644))
- if !(IsMkDocsProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("false without mkdocs.yml", func(t *testing.T) {
- dir := t.TempDir()
- if IsMkDocsProject(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsMkDocsProject_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("false for non-existent directory", func(t *testing.T) {
- if IsMkDocsProject(fs, "/non/existent/path") {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsMkDocsProject_Ugly(t *testing.T) {
- fs := storage.Local
- t.Run("false when mkdocs.yml is a directory", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.Mkdir(ax.Join(dir, "mkdocs.yml"), 0755))
- if IsMkDocsProject(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_HasSubtreeNpm_Good(t *testing.T) {
- fs := storage.Local
- t.Run("true with depth 1 nested package.json", func(t *testing.T) {
- dir := t.TempDir()
- subdir := ax.Join(dir, "packages", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(subdir, 0755))
-
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "packages", "package.json"), []byte("{}"), 0644))
- if !(HasSubtreeNpm(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with depth 2 nested package.json", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "apps", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0755))
-
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "package.json"), []byte("{}"), 0644))
- if !(HasSubtreeNpm(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("false with only root package.json", func(t *testing.T) {
- dir := setupTestDir(t, "package.json")
- if HasSubtreeNpm(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("false with only frontend package.json", func(t *testing.T) {
- dir := t.TempDir()
- frontendDir := ax.Join(dir, "frontend")
- requireDiscoveryOKResult(t, ax.MkdirAll(frontendDir, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte("{}"), 0o644))
- if HasSubtreeNpm(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("false with empty directory", func(t *testing.T) {
- dir := t.TempDir()
- if HasSubtreeNpm(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_HasSubtreeNpm_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("false for non-existent directory", func(t *testing.T) {
- if HasSubtreeNpm(fs, "/non/existent/path") {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("ignores node_modules at depth 1", func(t *testing.T) {
- dir := t.TempDir()
- nmDir := ax.Join(dir, "node_modules", "some-pkg")
- requireDiscoveryOKResult(t, ax.MkdirAll(nmDir, 0755))
-
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nmDir, "package.json"), []byte("{}"), 0644))
- if HasSubtreeNpm(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("ignores node_modules at depth 2", func(t *testing.T) {
- dir := t.TempDir()
- nmDir := ax.Join(dir, "apps", "node_modules", "some-pkg")
- requireDiscoveryOKResult(t, ax.MkdirAll(nmDir, 0755))
-
- // Also need the apps dir to be listable; it is since nmDir is inside it.
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nmDir, "package.json"), []byte("{}"), 0644))
- if HasSubtreeNpm(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("ignores hidden directories", func(t *testing.T) {
- dir := t.TempDir()
- hiddenDir := ax.Join(dir, ".turbo", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(hiddenDir, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(hiddenDir, "package.json"), []byte("{}"), 0o644))
- if HasSubtreeNpm(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_HasSubtreeNpm_Ugly(t *testing.T) {
- fs := storage.Local
- t.Run("false when nested package.json is beyond depth 2", func(t *testing.T) {
- dir := t.TempDir()
- deep := ax.Join(dir, "a", "b", "c")
- requireDiscoveryOKResult(t, ax.MkdirAll(deep, 0755))
-
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(deep, "package.json"), []byte("{}"), 0644))
- if HasSubtreeNpm(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsPythonProject_Good(t *testing.T) {
- fs := storage.Local
- t.Run("true with pyproject.toml", func(t *testing.T) {
- dir := setupTestDir(t, "pyproject.toml")
- if !(IsPythonProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with requirements.txt", func(t *testing.T) {
- dir := setupTestDir(t, "requirements.txt")
- if !(IsPythonProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("true with both markers", func(t *testing.T) {
- dir := setupTestDir(t, "pyproject.toml", "requirements.txt")
- if !(IsPythonProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("false without markers", func(t *testing.T) {
- dir := t.TempDir()
- if IsPythonProject(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsPythonProject_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("false for non-existent directory", func(t *testing.T) {
- if IsPythonProject(fs, "/non/existent/path") {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsPythonProject_Ugly(t *testing.T) {
- fs := storage.Local
- t.Run("false when pyproject.toml is a directory", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.Mkdir(ax.Join(dir, "pyproject.toml"), 0755))
- if IsPythonProject(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsRustProject_Good(t *testing.T) {
- fs := storage.Local
- t.Run("true with Cargo.toml", func(t *testing.T) {
- dir := setupTestDir(t, "Cargo.toml")
- if !(IsRustProject(fs, dir)) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("false without Cargo.toml", func(t *testing.T) {
- dir := t.TempDir()
- if IsRustProject(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsRustProject_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("false for non-existent directory", func(t *testing.T) {
- if IsRustProject(fs, "/non/existent/path") {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_IsRustProject_Ugly(t *testing.T) {
- fs := storage.Local
- t.Run("false when Cargo.toml is a directory", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.Mkdir(ax.Join(dir, "Cargo.toml"), 0755))
- if IsRustProject(fs, dir) {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestDiscovery_DiscoverFull_Good(t *testing.T) {
- fs := storage.Local
- t.Run("configured build type stays authoritative in full discovery", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, ".core"), 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, ".core", "build.yaml"), []byte("build:\n type: docker\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeDocker}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeDocker}, result.Types)
- }
- if !stdlibAssertEqual("docker", result.ConfiguredType) {
- t.Fatalf("want %v, got %v", "docker", result.ConfiguredType)
- }
- if !stdlibAssertEqual("docker", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "docker", result.PrimaryStack)
- }
- if !stdlibAssertEqual("docker", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "docker", result.SuggestedStack)
- }
- if !stdlibAssertEqual("docker", result.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "docker", result.PrimaryStackSuggestion)
- }
- if !(result.Markers["go.mod"]) {
- t.Fatal("expected true")
- }
- if !(result.Markers["wails.json"]) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns complete result for Go project", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod", "main.go")
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeGo}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeGo}, result.Types)
- }
- if !stdlibAssertEqual(runtime.GOOS, result.OS) {
- t.Fatalf("want %v, got %v", runtime.GOOS, result.OS)
- }
- if !stdlibAssertEqual(runtime.GOARCH, result.Arch) {
- t.Fatalf("want %v, got %v", runtime.GOARCH, result.Arch)
- }
- if !stdlibAssertEqual("go", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "go", result.PrimaryStack)
- }
- if !stdlibAssertEqual("go", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "go", result.SuggestedStack)
- }
- if result.HasFrontend {
- t.Fatal("expected false")
- }
- if result.HasRootPackageJSON {
- t.Fatal("expected false")
- }
- if result.HasFrontendPackageJSON {
- t.Fatal("expected false")
- }
- if !(result.HasRootGoMod) {
- t.Fatal("expected true")
- }
- if !(result.HasRootMainGo) {
- t.Fatal("expected true")
- }
- if result.HasRootCMakeLists {
- t.Fatal("expected false")
- }
- if result.HasSubtreeNpm {
- t.Fatal("expected false")
- }
- if !(result.Markers["go.mod"]) {
- t.Fatal("expected true")
- }
- if !(result.Markers["main.go"]) {
- t.Fatal("expected true")
- }
- if result.Markers["wails.json"] {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("detects nested MkDocs configuration", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "docs", "guide")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "mkdocs.yaml"), []byte("site_name: Guide"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !(result.HasDocsConfig) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("docs", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "docs", result.PrimaryStack)
- }
- if !stdlibAssertEqual("docs", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "docs", result.SuggestedStack)
- }
-
- })
-
- t.Run("prefers Go stack suggestion when docs and Go toolchain coexist", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "mkdocs.yml"), []byte("site_name: Demo\n"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeGo, ProjectTypeDocs}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeGo, ProjectTypeDocs}, result.Types)
- }
- if !(result.HasDocsConfig) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("go", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "go", result.PrimaryStack)
- }
- if !stdlibAssertEqual("go", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "go", result.SuggestedStack)
- }
- if !stdlibAssertEqual("go", result.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "go", result.PrimaryStackSuggestion)
- }
- if !(result.Markers["go.mod"]) {
- t.Fatal("expected true")
- }
- if !(result.Markers["mkdocs.yml"]) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("captures GitHub metadata when available", func(t *testing.T) {
- t.Setenv("GITHUB_SHA", "0123456789abcdef")
- t.Setenv("GITHUB_REF", "refs/tags/v1.2.3")
- t.Setenv("GITHUB_REPOSITORY", "dappcore/core")
-
- dir := setupTestDir(t, "go.mod")
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual("refs/tags/v1.2.3", result.Ref) {
- t.Fatalf("want %v, got %v", "refs/tags/v1.2.3", result.Ref)
- }
- if !stdlibAssertEqual("v1.2.3", result.Tag) {
- t.Fatalf("want %v, got %v", "v1.2.3", result.Tag)
- }
- if !(result.IsTag) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("0123456789abcdef", result.SHA) {
- t.Fatalf("want %v, got %v", "0123456789abcdef", result.SHA)
- }
- if !stdlibAssertEqual("0123456", result.ShortSHA) {
- t.Fatalf("want %v, got %v", "0123456", result.ShortSHA)
- }
- if !stdlibAssertEqual("dappcore/core", result.Repo) {
- t.Fatalf("want %v, got %v", "dappcore/core", result.Repo)
- }
- if !stdlibAssertEqual("dappcore", result.Owner) {
- t.Fatalf("want %v, got %v", "dappcore", result.Owner)
- }
-
- })
-
- t.Run("falls back to local git metadata when GitHub env is absent", func(t *testing.T) {
- t.Setenv("GITHUB_SHA", "")
- t.Setenv("GITHUB_REF", "")
- t.Setenv("GITHUB_REPOSITORY", "")
-
- dir, sha := initGitMetadataRepo(t)
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual(sha, result.SHA) {
- t.Fatalf("want %v, got %v", sha, result.SHA)
- }
- if !stdlibAssertEqual(sha[:7], result.ShortSHA) {
- t.Fatalf("want %v, got %v", sha[:7], result.ShortSHA)
- }
- if !stdlibAssertEqual("refs/heads/main", result.Ref) {
- t.Fatalf("want %v, got %v", "refs/heads/main", result.Ref)
- }
- if !stdlibAssertEqual("main", result.Branch) {
- t.Fatalf("want %v, got %v", "main", result.Branch)
- }
- if result.IsTag {
- t.Fatal("expected false")
- }
- if !stdlibAssertEqual("dappcore/core", result.Repo) {
- t.Fatalf("want %v, got %v", "dappcore/core", result.Repo)
- }
- if !stdlibAssertEqual("dappcore", result.Owner) {
- t.Fatalf("want %v, got %v", "dappcore", result.Owner)
- }
-
- })
-
- t.Run("returns complete result for Go workspace project", func(t *testing.T) {
- dir := setupTestDir(t, "go.work")
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeGo}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeGo}, result.Types)
- }
- if !stdlibAssertEqual("go", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "go", result.PrimaryStack)
- }
- if !(result.Markers[
-
- // Create wails.json, go.mod, and frontend/package.json
- "go.work"]) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("returns complete result for Wails project with frontend", func(t *testing.T) {
- dir := t.TempDir()
-
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0644))
-
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("{}"), 0644))
-
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, "frontend"), 0755))
-
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "frontend", "package.json"), []byte("{}"), 0644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types)
- }
- if !stdlibAssertEqual("wails", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "wails", result.PrimaryStack)
- }
- if !stdlibAssertEqual("wails2", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "wails2", result.SuggestedStack)
- }
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
- if result.HasRootPackageJSON {
- t.Fatal("expected false")
- }
- if !(result.HasFrontendPackageJSON) {
- t.Fatal("expected true")
- }
- if !(result.HasRootGoMod) {
- t.Fatal("expected true")
- }
- if result.HasRootMainGo {
- t.Fatal("expected false")
- }
- if result.HasRootCMakeLists {
- t.Fatal("expected false")
- }
- if result.HasSubtreeNpm {
- t.Fatal("expected false")
- }
- if !(result.Markers["wails.json"]) {
- t.Fatal("expected true")
- }
- if !(result.Markers["go.mod"]) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects subtree npm as frontend", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("{}"), 0644))
-
- nested := ax.Join(dir, "apps", "web")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0755))
-
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "package.json"), []byte("{}"), 0644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types)
- }
- if !stdlibAssertEqual("wails", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "wails", result.PrimaryStack)
- }
- if !(result.HasSubtreeNpm) {
- t.Fatal("expected true")
- }
- if !(result.HasSubtreePackageJSON) {
- t.Fatal("expected true")
- }
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("detects root package.json as frontend", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeNode}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeNode}, result.Types)
- }
- if !stdlibAssertEqual("node", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "node", result.PrimaryStack)
- }
- if !stdlibAssertEqual("node", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "node", result.SuggestedStack)
- }
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
- if !(result.HasRootPackageJSON) {
- t.Fatal("expected true")
- }
- if result.HasFrontendPackageJSON {
- t.Fatal("expected false")
- }
- if result.HasRootComposerJSON {
- t.Fatal("expected false")
- }
- if result.HasRootCargoToml {
- t.Fatal("expected false")
- }
- if result.HasRootGoMod {
- t.Fatal("expected false")
- }
- if result.HasRootMainGo {
- t.Fatal("expected false")
- }
- if result.HasRootCMakeLists {
- t.Fatal("expected false")
- }
- if result.HasTaskfile {
- t.Fatal("expected false")
- }
- if result.HasSubtreeNpm {
- t.Fatal("expected false")
- }
- if result.HasSubtreePackageJSON {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("detects root deno.json as node project", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "deno.json"), []byte("{}"), 0644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeNode}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeNode}, result.Types)
- }
- if !stdlibAssertEqual("node", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "node", result.PrimaryStack)
- }
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
- if !(result.Markers["deno.json"]) {
- t.Fatal("expected true")
- }
- if result.Markers["package.json"] {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("detects go.mod with root package.json as Wails", func(t *testing.T) {
- dir := setupTestDir(t, "go.mod", "package.json")
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types)
- }
- if !stdlibAssertEqual("wails", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "wails", result.PrimaryStack)
- }
- if !stdlibAssertEqual("wails2", result.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "wails2", result.PrimaryStackSuggestion)
- }
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
- if !(result.HasPackageJSON) {
- t.Fatal("expected true")
- }
- if result.HasDenoManifest {
- t.Fatal("expected false")
- }
- if !(result.HasGoToolchain) {
- t.Fatal("expected true")
- }
- if result.HasRootGoWork {
- t.Fatal("expected false")
- }
- if result.HasRootWailsJSON {
- t.Fatal("expected false")
- }
- if result.HasSubtreeNpm {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("detects frontend deno manifest at project root", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("{}"), 0644))
-
- frontendDir := ax.Join(dir, "frontend")
- requireDiscoveryOKResult(t, ax.MkdirAll(frontendDir, 0755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte("{}"), 0644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types)
- }
- if !stdlibAssertEqual("wails", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "wails", result.PrimaryStack)
- }
- if !stdlibAssertEqual("wails2", result.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "wails2", result.PrimaryStackSuggestion)
- }
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
- if result.HasPackageJSON {
- t.Fatal("expected false")
- }
- if !(result.HasDenoManifest) {
- t.Fatal("expected true")
- }
- if result.HasSubtreeNpm {
- t.Fatal("expected false")
- }
- if !(result.Markers["frontend/deno.json"]) {
- t.Fatal("expected true")
- }
- if result.Markers["frontend/package.json"] {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("detects nested deno frontend manifests", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("{}"), 0644))
-
- frontendDir := ax.Join(dir, "apps", "site")
- requireDiscoveryOKResult(t, ax.MkdirAll(frontendDir, 0755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontendDir, "deno.jsonc"), []byte("{}"), 0644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, result.Types)
- }
- if !stdlibAssertEqual("wails", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "wails", result.PrimaryStack)
- }
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
- if result.HasSubtreeNpm {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("detects nested deno project as node", func(t *testing.T) {
- dir := t.TempDir()
- frontendDir := ax.Join(dir, "apps", "site")
- requireDiscoveryOKResult(t, ax.MkdirAll(frontendDir, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontendDir, "deno.jsonc"), []byte("{}"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeNode}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeNode}, result.Types)
- }
- if !stdlibAssertEqual("node", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "node", result.PrimaryStack)
- }
- if !stdlibAssertEqual("node", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "node", result.SuggestedStack)
- }
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
- if result.HasSubtreeNpm {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("detects nested deno subtree manifests in full discovery", func(t *testing.T) {
- dir := t.TempDir()
- frontendDir := ax.Join(dir, "apps", "site")
- requireDiscoveryOKResult(t, ax.MkdirAll(frontendDir, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontendDir, "deno.json"), []byte("{}"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeNode}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeNode}, result.Types)
- }
- if !stdlibAssertEqual("node", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "node", result.PrimaryStack)
- }
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
- if !(result.HasDenoManifest) {
- t.Fatal("expected true")
- }
- if !(result.HasSubtreeDenoManifest) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("records frontend package manifest markers", func(t *testing.T) {
- dir := t.TempDir()
- frontendDir := ax.Join(dir, "frontend")
- requireDiscoveryOKResult(t, ax.MkdirAll(frontendDir, 0755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte("{}"), 0644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(frontendDir, "deno.jsonc"), []byte("{}"), 0644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !(result.HasFrontend) {
- t.Fatal("expected true")
- }
- if !(result.Markers["frontend/package.json"]) {
- t.Fatal("expected true")
- }
- if !(result.Markers["frontend/deno.jsonc"]) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("records the build config marker and prefers configured type", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, ".core"), 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, ".core", "build.yaml"), []byte("build:\n type: cpp\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "Dockerfile"), []byte("FROM alpine\n"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeCPP}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeCPP}, result.Types)
- }
- if !stdlibAssertEqual("cpp", result.ConfiguredType) {
- t.Fatalf("want %v, got %v", "cpp", result.ConfiguredType)
- }
- if !stdlibAssertEqual("cpp", result.ConfiguredBuildType) {
- t.Fatalf("want %v, got %v", "cpp", result.ConfiguredBuildType)
- }
- if !stdlibAssertEqual("cpp", result.PrimaryStack) {
- t.Fatalf("want %v, got %v", "cpp", result.PrimaryStack)
- }
- if !stdlibAssertEqual("cpp", result.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "cpp", result.PrimaryStackSuggestion)
- }
- if !(result.Markers[".core/build.yaml"]) {
- t.Fatal("expected true")
- }
- if !(result.Markers["Dockerfile"]) {
- t.Fatal("expected true")
- }
-
- })
-
- t.Run("records workflow-facing marker aliases", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "composer.json"), []byte("{}"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "Cargo.toml"), []byte("[package]\nname = \"demo\"\nversion = \"0.1.0\"\n"), 0o644))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, "Taskfile.yaml"), []byte("version: '3'\n"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !(result.HasRootComposerJSON) {
- t.Fatal("expected true")
- }
- if !(result.HasRootCargoToml) {
- t.Fatal("expected true")
- }
- if !(result.HasTaskfile) {
- t.Fatal("expected true")
- }
- if result.HasSubtreePackageJSON {
- t.Fatal("expected false")
- }
-
- })
-
- t.Run("maps configured wails type to the action stack suggestion", func(t *testing.T) {
- dir := t.TempDir()
- requireDiscoveryOKResult(t, ax.MkdirAll(ax.Join(dir, ".core"), 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(dir, ".core", "build.yaml"), []byte("build:\n type: wails\n"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails}, result.Types) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails}, result.Types)
- }
- if !stdlibAssertEqual("wails", result.ConfiguredType) {
- t.Fatalf("want %v, got %v", "wails", result.ConfiguredType)
- }
- if !stdlibAssertEqual("wails2", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "wails2", result.SuggestedStack)
- }
- if !stdlibAssertEqual("wails2", result.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "wails2", result.PrimaryStackSuggestion)
- }
-
- })
-
- t.Run("reports distro-aware Linux packages for Wails projects", func(t *testing.T) {
- mock := storage.NewMemoryMedium()
- requireDiscoveryOKResult(t, mock.EnsureDir("/project"))
- requireDiscoveryOKResult(t, mock.Write("/project/go.mod", "module example"))
- requireDiscoveryOKResult(t, mock.Write("/project/package.json", "{}"))
- requireDiscoveryOKResult(t, mock.Write("/etc/os-release", "ID=ubuntu\nVERSION_ID=\"24.04\"\n"))
-
- result := requireDiscoveryFull(t, DiscoverFull(mock, "/project"))
- if !stdlibAssertEqual([]string{"libwebkit2gtk-4.1-dev"}, result.LinuxPackages) {
- t.Fatalf("want %v, got %v", []string{"libwebkit2gtk-4.1-dev"}, result.LinuxPackages)
- }
- if !stdlibAssertEqual("libwebkit2gtk-4.1-dev", result.WebKitPackage) {
- t.Fatalf("want %v, got %v", "libwebkit2gtk-4.1-dev", result.WebKitPackage)
- }
-
- })
-
- t.Run("empty directory returns empty result", func(t *testing.T) {
- dir := t.TempDir()
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEmpty(result.Types) {
- t.Fatalf("expected empty, got %v", result.Types)
- }
- if !stdlibAssertEmpty(result.PrimaryStack) {
- t.Fatalf("expected empty, got %v", result.PrimaryStack)
- }
- if !stdlibAssertEqual("unknown", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "unknown", result.SuggestedStack)
- }
- if result.HasFrontend {
- t.Fatal("expected false")
- }
- if result.HasRootPackageJSON {
- t.Fatal("expected false")
- }
- if result.HasFrontendPackageJSON {
- t.Fatal("expected false")
- }
- if result.HasRootComposerJSON {
- t.Fatal("expected false")
- }
- if result.HasRootCargoToml {
- t.Fatal("expected false")
- }
- if result.HasPackageJSON {
- t.Fatal("expected false")
- }
- if result.HasDenoManifest {
- t.Fatal("expected false")
- }
- if result.HasRootGoMod {
- t.Fatal("expected false")
- }
- if result.HasRootGoWork {
- t.Fatal("expected false")
- }
- if result.HasRootMainGo {
- t.Fatal("expected false")
- }
- if result.HasRootCMakeLists {
- t.Fatal("expected false")
- }
- if result.HasRootWailsJSON {
- t.Fatal("expected false")
- }
- if result.HasTaskfile {
- t.Fatal("expected false")
- }
- if result.HasSubtreeNpm {
- t.Fatal("expected false")
- }
- if result.HasSubtreePackageJSON {
- t.Fatal("expected false")
- }
- if result.HasSubtreeDenoManifest {
- t.Fatal("expected false")
- }
- if result.HasDocsConfig {
- t.Fatal("expected false")
- }
- if result.HasGoToolchain {
- t.Fatal("expected false")
- }
- if !stdlibAssertEqual("unknown", result.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "unknown", result.PrimaryStackSuggestion)
- }
- if !stdlibAssertEmpty(result.WebKitPackage) {
- t.Fatalf("expected empty, got %v", result.WebKitPackage)
- }
-
- })
-
- for _, tc := range []struct {
- name string
- setup func(t *testing.T) string
- want []ProjectType
- stack string
- markers []string
- check func(t *testing.T, result *DiscoveryResult)
- }{
- {
- name: "detects docs project markers",
- setup: func(t *testing.T) string { return setupTestDir(t, "mkdocs.yml") },
- want: []ProjectType{ProjectTypeDocs},
- stack: "docs",
- markers: []string{"mkdocs.yml"},
- check: func(t *testing.T, result *DiscoveryResult) {
- t.Helper()
- if !stdlibAssertEqual("docs", result.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "docs", result.PrimaryStackSuggestion)
- }
- if !result.HasDocsConfig {
- t.Fatal("expected true")
- }
- },
- },
- {
- name: "detects docs project markers with mkdocs.yaml",
- setup: func(t *testing.T) string { return setupTestDir(t, "mkdocs.yaml") },
- want: []ProjectType{ProjectTypeDocs},
- stack: "docs",
- markers: []string{"mkdocs.yaml"},
- },
- {
- name: "detects docs project markers in docs directory",
- setup: func(t *testing.T) string { return setupDiscoveryFile(t, "docs/mkdocs.yaml", "site_name: Demo\n") },
- want: []ProjectType{ProjectTypeDocs},
- stack: "docs",
- markers: []string{"docs/mkdocs.yaml"},
- },
- {
- name: "detects Rust project markers",
- setup: func(t *testing.T) string { return setupTestDir(t, "Cargo.toml") },
- want: []ProjectType{ProjectTypeRust},
- stack: "rust",
- markers: []string{"Cargo.toml"},
- },
- {
- name: "detects Python project markers",
- setup: func(t *testing.T) string { return setupTestDir(t, "pyproject.toml") },
- want: []ProjectType{ProjectTypePython},
- stack: "python",
- markers: []string{"pyproject.toml"},
- },
- {
- name: "detects Docker project markers",
- setup: func(t *testing.T) string { return setupTestDir(t, "Dockerfile") },
- want: []ProjectType{ProjectTypeDocker},
- stack: "docker",
- markers: []string{"Dockerfile"},
- },
- {
- name: "records alternate Docker manifest markers",
- setup: func(t *testing.T) string { return setupTestDir(t, "Containerfile", "dockerfile", "containerfile") },
- want: []ProjectType{ProjectTypeDocker},
- stack: "docker",
- markers: []string{"Containerfile", "dockerfile", "containerfile"},
- },
- {
- name: "detects LinuxKit project markers in .core/linuxkit",
- setup: func(t *testing.T) string {
- return setupDiscoveryFile(t, ".core/linuxkit/server.yml", "kernel:\n image: test")
- },
- want: []ProjectType{ProjectTypeLinuxKit},
- stack: "linuxkit",
- markers: []string{".core/linuxkit/*.yml", ".core/linuxkit/*.yaml"},
- },
- {
- name: "detects LinuxKit project markers in linuxkit.yaml",
- setup: func(t *testing.T) string { return setupTestDir(t, "linuxkit.yaml") },
- want: []ProjectType{ProjectTypeLinuxKit},
- stack: "linuxkit",
- markers: []string{"linuxkit.yaml"},
- },
- {
- name: "detects C++ project markers",
- setup: func(t *testing.T) string { return setupTestDir(t, "CMakeLists.txt") },
- want: []ProjectType{ProjectTypeCPP},
- stack: "cpp",
- markers: []string{"CMakeLists.txt"},
- },
- {
- name: "detects Taskfile project markers",
- setup: func(t *testing.T) string { return setupTestDir(t, "Taskfile.yaml") },
- want: []ProjectType{ProjectTypeTaskfile},
- stack: "taskfile",
- markers: []string{"Taskfile.yaml"},
- },
- } {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- result := assertDiscoverFullStack(t, fs, tc.setup(t), tc.want, tc.stack, tc.markers...)
- if tc.check != nil {
- tc.check(t, result)
- }
- })
- }
-
- t.Run("reports nested Go toolchains for action parity even when root detection is empty", func(t *testing.T) {
- dir := t.TempDir()
- nested := ax.Join(dir, "services", "api")
- requireDiscoveryOKResult(t, ax.MkdirAll(nested, 0o755))
- requireDiscoveryOKResult(t, ax.WriteFile(ax.Join(nested, "go.mod"), []byte("module example/api\n"), 0o644))
-
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if !stdlibAssertEmpty(result.Types) {
- t.Fatalf("expected empty, got %v", result.Types)
- }
- if !stdlibAssertEmpty(result.PrimaryStack) {
- t.Fatalf("expected empty, got %v", result.PrimaryStack)
- }
- if !stdlibAssertEqual("unknown", result.SuggestedStack) {
- t.Fatalf("want %v, got %v", "unknown", result.SuggestedStack)
- }
- if !(result.HasGoToolchain) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("go", result.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "go", result.PrimaryStackSuggestion)
- }
-
- })
-}
-
-func TestDiscovery_DiscoverFull_Bad(t *testing.T) {
- fs := storage.Local
- t.Run("non-existent directory returns empty result", func(t *testing.T) {
- result := requireDiscoveryFull(t, DiscoverFull(fs, "/non/existent/path"))
- if !stdlibAssertEmpty(result.Types) {
- t.Fatalf("expected empty, got %v", result.Types)
- }
- if !stdlibAssertEmpty(result.PrimaryStack) {
- t.Fatalf("expected empty, got %v", result.PrimaryStack)
- }
-
- })
-}
-
-func TestDiscovery_DiscoverFull_Ugly(t *testing.T) {
- fs := storage.Local
- t.Run("markers map is never nil even for empty directory", func(t *testing.T) {
- dir := t.TempDir()
- result := requireDiscoveryFull(t, DiscoverFull(fs, dir))
- if stdlibAssertNil(result.Markers) {
- t.Fatal("expected non-nil")
- }
-
- })
-}
-
-func TestDiscovery_SuggestStack_Good(t *testing.T) {
- t.Run("maps Wails projects to the v3 action stack name", func(t *testing.T) {
- if !stdlibAssertEqual("wails2", SuggestStack([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode})) {
- t.Fatalf("want %v, got %v", "wails2", SuggestStack([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}))
- }
-
- })
-
- t.Run("passes through non-Wails primary project types", func(t *testing.T) {
- if !stdlibAssertEqual("cpp", SuggestStack([]ProjectType{ProjectTypeCPP})) {
- t.Fatalf("want %v, got %v", "cpp", SuggestStack([]ProjectType{ProjectTypeCPP}))
- }
- if !stdlibAssertEqual("docs", SuggestStack([]ProjectType{ProjectTypeDocs})) {
- t.Fatalf("want %v, got %v", "docs", SuggestStack([]ProjectType{ProjectTypeDocs}))
- }
- if !stdlibAssertEqual("node", SuggestStack([]ProjectType{ProjectTypeNode})) {
- t.Fatalf("want %v, got %v", "node", SuggestStack([]ProjectType{ProjectTypeNode}))
- }
- if !stdlibAssertEqual("go", SuggestStack([]ProjectType{ProjectTypeGo})) {
- t.Fatalf("want %v, got %v", "go", SuggestStack([]ProjectType{ProjectTypeGo}))
- }
-
- })
-
- t.Run("returns empty when nothing is detected", func(t *testing.T) {
- if !stdlibAssertEqual("unknown", SuggestStack(nil)) {
- t.Fatalf("want %v, got %v", "unknown", SuggestStack(nil))
- }
-
- })
-}
-
-func TestDiscovery_ResolveLinuxPackages_Good(t *testing.T) {
- t.Run("returns Ubuntu 24.04 WebKit package for Wails", func(t *testing.T) {
- packages := ResolveLinuxPackages([]ProjectType{ProjectTypeWails}, "24.04")
- if !stdlibAssertEqual([]string{"libwebkit2gtk-4.1-dev"}, packages) {
- t.Fatalf("want %v, got %v", []string{"libwebkit2gtk-4.1-dev"}, packages)
- }
-
- })
-
- t.Run("returns Ubuntu 22.04 WebKit package for Wails", func(t *testing.T) {
- packages := ResolveLinuxPackages([]ProjectType{ProjectTypeWails}, "22.04")
- if !stdlibAssertEqual([]string{"libwebkit2gtk-4.0-dev"}, packages) {
- t.Fatalf("want %v, got %v", []string{"libwebkit2gtk-4.0-dev"}, packages)
- }
-
- })
-
- t.Run("returns no Linux packages for non-Wails stacks", func(t *testing.T) {
- packages := ResolveLinuxPackages([]ProjectType{ProjectTypeGo}, "24.04")
- if !stdlibAssertEmpty(packages) {
- t.Fatalf("expected empty, got %v", packages)
- }
-
- })
-}
-
-func TestDiscovery_ParseOSReleaseDistroGood(t *testing.T) {
- t.Run("returns ubuntu version id", func(t *testing.T) {
- content := `
-NAME="Ubuntu"
-ID=ubuntu
-VERSION_ID="24.04"
-ID_LIKE=debian
-`
- if !stdlibAssertEqual("24.04", parseOSReleaseDistro(content)) {
- t.Fatalf("want %v, got %v", "24.04", parseOSReleaseDistro(content))
- }
-
- })
-
- t.Run("accepts ubuntu-style values without quotes", func(t *testing.T) {
- content := `
-ID=ubuntu
-VERSION_ID=25.10
-`
- if !stdlibAssertEqual("25.10", parseOSReleaseDistro(content)) {
- t.Fatalf("want %v, got %v", "25.10", parseOSReleaseDistro(content))
- }
-
- })
-}
-
-func TestDiscovery_ParseOSReleaseDistroBad(t *testing.T) {
- t.Run("returns empty for non-ubuntu distro", func(t *testing.T) {
- content := `
-ID=fedora
-VERSION_ID=41
-`
- if !stdlibAssertEmpty(parseOSReleaseDistro(content)) {
- t.Fatalf("expected empty, got %v", parseOSReleaseDistro(content))
- }
-
- })
-
- t.Run("returns empty when version missing", func(t *testing.T) {
- content := `
-ID=ubuntu
-`
- if !stdlibAssertEmpty(parseOSReleaseDistro(content)) {
- t.Fatalf("expected empty, got %v", parseOSReleaseDistro(content))
- }
-
- })
-}
-
-func TestDiscovery_DetectDistroVersionGood(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireDiscoveryOKResult(t, fs.Write("/etc/os-release", `
-ID=ubuntu
-VERSION_ID="24.04"
-`))
- if !stdlibAssertEqual("24.04", detectDistroVersion(fs)) {
- t.Fatalf("want %v, got %v", "24.04", detectDistroVersion(fs))
- }
-
-}
-
-func TestDiscovery_DetectDistroVersionBad(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireDiscoveryOKResult(t, fs.Write("/etc/os-release", `
-ID=fedora
-VERSION_ID=41
-`))
- if !stdlibAssertEmpty(detectDistroVersion(fs)) {
- t.Fatalf("expected empty, got %v", detectDistroVersion(fs))
- }
-
-}
-
-func TestDiscovery_NilMediumGood(t *testing.T) {
- dir := t.TempDir()
-
- types := requireDiscoveryTypes(t, Discover(nil, dir))
- if !stdlibAssertEmpty(types) {
- t.Fatalf("expected empty, got %v", types)
- }
-
- result := requireDiscoveryFull(t, DiscoverFull(nil, dir))
- if stdlibAssertNil(result) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEmpty(result.Types) {
- t.Fatalf("expected empty, got %v", result.Types)
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestDiscovery_Discover_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Discover(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_PrimaryType_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = PrimaryType(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_PrimaryType_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = PrimaryType(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_IsGoProject_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsGoProject(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_IsGoProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsGoProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_IsWailsProject_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsWailsProject(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_IsWailsProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsWailsProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_IsNodeProject_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsNodeProject(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_IsNodeProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsNodeProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_IsPHPProject_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsPHPProject(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_IsPHPProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsPHPProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_IsCPPProject_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsCPPProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDiscovery_IsCPPProject_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsCPPProject(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_IsCPPProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsCPPProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_IsDocsProject_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsDocsProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDiscovery_IsDocsProject_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsDocsProject(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_IsDocsProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsDocsProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_ResolveMkDocsConfigPath_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveMkDocsConfigPath(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDiscovery_ResolveMkDocsConfigPath_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveMkDocsConfigPath(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_ResolveMkDocsConfigPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveMkDocsConfigPath(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_SuggestStack_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = SuggestStack(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_SuggestStack_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = SuggestStack(nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_ResolveLinuxPackages_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveLinuxPackages(nil, "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_ResolveLinuxPackages_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveLinuxPackages(nil, "agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_ResolveDockerfilePath_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveDockerfilePath(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDiscovery_ResolveDockerfilePath_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveDockerfilePath(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_ResolveDockerfilePath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveDockerfilePath(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_IsDockerProject_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsDockerProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDiscovery_IsDockerProject_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsDockerProject(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_IsDockerProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsDockerProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_IsLinuxKitProject_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsLinuxKitProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDiscovery_IsLinuxKitProject_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsLinuxKitProject(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_IsLinuxKitProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsLinuxKitProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestDiscovery_IsTaskfileProject_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsTaskfileProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestDiscovery_IsTaskfileProject_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsTaskfileProject(storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestDiscovery_IsTaskfileProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = IsTaskfileProject(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/env.go b/pkg/build/env.go
deleted file mode 100644
index 14c7b89..0000000
--- a/pkg/build/env.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package build
-
-import "dappco.re/go"
-
-// BuildEnvironment returns a fresh environment slice that includes the
-// configured build environment, any derived cache variables, and optional
-// builder-specific values.
-func BuildEnvironment(cfg *Config, extra ...string) []string {
- if cfg == nil {
- if len(extra) == 0 {
- return nil
- }
- return append([]string{}, extra...)
- }
-
- env := append([]string{}, cfg.Env...)
- env = append(env, CacheEnvironment(&cfg.Cache)...)
- env = append(env, extra...)
-
- if len(env) == 0 {
- return nil
- }
-
- return env
-}
-
-// DenoRequested reports whether the current build should prefer a Deno-backed
-// frontend build. It honours the action-style environment overrides first and
-// then the persisted/configured command override.
-func DenoRequested(configuredBuild string) bool {
- if truthyEnv(core.Env("DENO_ENABLE")) {
- return true
- }
-
- if core.Trim(core.Env("DENO_BUILD")) != "" {
- return true
- }
-
- return core.Trim(configuredBuild) != ""
-}
-
-// NpmRequested reports whether the current build should prefer an npm-backed
-// frontend build. It honours the action-style environment override first and
-// then the persisted/configured command override.
-func NpmRequested(configuredBuild string) bool {
- if core.Trim(core.Env("NPM_BUILD")) != "" {
- return true
- }
-
- return core.Trim(configuredBuild) != ""
-}
-
-func truthyEnv(value string) bool {
- switch core.Lower(core.Trim(value)) {
- case "1", "true", "yes", "on":
- return true
- default:
- return false
- }
-}
diff --git a/pkg/build/env_example_test.go b/pkg/build/env_example_test.go
deleted file mode 100644
index 81f67c7..0000000
--- a/pkg/build/env_example_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleBuildEnvironment references BuildEnvironment on this package API surface.
-func ExampleBuildEnvironment() {
- _ = BuildEnvironment
- core.Println("BuildEnvironment")
- // Output: BuildEnvironment
-}
-
-// ExampleDenoRequested references DenoRequested on this package API surface.
-func ExampleDenoRequested() {
- _ = DenoRequested
- core.Println("DenoRequested")
- // Output: DenoRequested
-}
-
-// ExampleNpmRequested references NpmRequested on this package API surface.
-func ExampleNpmRequested() {
- _ = NpmRequested
- core.Println("NpmRequested")
- // Output: NpmRequested
-}
diff --git a/pkg/build/env_test.go b/pkg/build/env_test.go
deleted file mode 100644
index 792f545..0000000
--- a/pkg/build/env_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-func TestEnv_BuildEnvironment_Good(t *core.T) {
- cfg := &Config{
- Env: []string{"APP_ENV=dev"},
- Cache: CacheConfig{
- Enabled: true,
- Paths: []string{"cache/go-build"},
- },
- }
-
- env := BuildEnvironment(cfg, "EXTRA=1")
- core.AssertContains(t, env, "APP_ENV=dev")
- core.AssertContains(t, env, "GOCACHE=cache/go-build")
- core.AssertContains(t, env, "EXTRA=1")
-}
-
-func TestEnv_BuildEnvironment_Bad(t *core.T) {
- env := BuildEnvironment(nil, "EXTRA=1")
- core.AssertLen(t, env, 1)
- core.AssertEqual(t, []string{"EXTRA=1"}, env)
-}
-
-func TestEnv_BuildEnvironment_Ugly(t *core.T) {
- env := BuildEnvironment(&Config{})
- core.AssertEmpty(t, env)
- core.AssertNil(t, env)
-}
-
-func TestEnv_DenoRequested_Good(t *core.T) {
- clearBuildEnv(t, "DENO_ENABLE", "DENO_BUILD")
- setBuildEnv(t, "DENO_ENABLE", "true")
- defer clearBuildEnv(t, "DENO_ENABLE")
-
- core.AssertTrue(t, DenoRequested(""))
-}
-
-func TestEnv_DenoRequested_Bad(t *core.T) {
- clearBuildEnv(t, "DENO_ENABLE", "DENO_BUILD")
- requested := DenoRequested("")
- core.AssertFalse(t, requested)
- core.AssertEqual(t, false, requested)
-}
-
-func TestEnv_DenoRequested_Ugly(t *core.T) {
- clearBuildEnv(t, "DENO_ENABLE", "DENO_BUILD")
- requested := DenoRequested(" deno task build ")
- core.AssertTrue(t, requested)
- core.AssertEqual(t, true, requested)
-}
-
-func TestEnv_NpmRequested_Good(t *core.T) {
- clearBuildEnv(t, "NPM_BUILD")
- setBuildEnv(t, "NPM_BUILD", "npm run build")
- defer clearBuildEnv(t, "NPM_BUILD")
-
- core.AssertTrue(t, NpmRequested(""))
-}
-
-func TestEnv_NpmRequested_Bad(t *core.T) {
- clearBuildEnv(t, "NPM_BUILD")
- requested := NpmRequested("")
- core.AssertFalse(t, requested)
- core.AssertEqual(t, false, requested)
-}
-
-func TestEnv_NpmRequested_Ugly(t *core.T) {
- clearBuildEnv(t, "NPM_BUILD")
- requested := NpmRequested(" npm run assets ")
- core.AssertTrue(t, requested)
- core.AssertEqual(t, true, requested)
-}
-
-func setBuildEnv(t *core.T, key, value string) {
- t.Helper()
- setenv := core.Setenv
- r := setenv(key, value)
- core.RequireTrue(t, r.OK, r.Error())
-}
-
-func clearBuildEnv(t *core.T, keys ...string) {
- t.Helper()
- unsetenv := core.Unsetenv
- for _, key := range keys {
- r := unsetenv(key)
- core.RequireTrue(t, r.OK, r.Error())
- }
-}
diff --git a/pkg/build/images/core-dev.yml b/pkg/build/images/core-dev.yml
deleted file mode 100644
index 4aa262b..0000000
--- a/pkg/build/images/core-dev.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-# core-dev LinuxKit image template
-kernel:
- image: linuxkit/kernel:6.6.13
- cmdline: "console=tty0 console=ttyS0"
-
-init:
- - linuxkit/init:v1.2.0
- - linuxkit/runc:v1.1.12
- - linuxkit/containerd:v1.7.13
- - linuxkit/ca-certificates:v1.0.0
-
-services:
- - name: core-dev
- image: "{{ .ServiceImage }}"
- net: host
- capabilities:
- - all
-{{- if .Mounts }}
- binds:
-{{- range .Mounts }}
- - {{ . }}:{{ . }}
-{{- end }}
-{{- end }}
- env:
- - CORE_IMAGE=core-dev
- - CORE_GPU={{ if .GPU }}1{{ else }}0{{ end }}
- command:
- - /bin/sh
- - -lc
- - '{{ .EntrypointCommand }}'
-
-files:
- - path: /etc/motd
- contents: |
- core-dev
- Version: {{ .Version }}
- {{ .Description }}
-
-trust:
- org:
- - linuxkit
- - library
diff --git a/pkg/build/images/core-minimal.yml b/pkg/build/images/core-minimal.yml
deleted file mode 100644
index 11445da..0000000
--- a/pkg/build/images/core-minimal.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-# core-minimal LinuxKit image template
-kernel:
- image: linuxkit/kernel:6.6.13
- cmdline: "console=tty0 console=ttyS0"
-
-init:
- - linuxkit/init:v1.2.0
- - linuxkit/runc:v1.1.12
- - linuxkit/containerd:v1.7.13
- - linuxkit/ca-certificates:v1.0.0
-
-services:
- - name: core-minimal
- image: "{{ .ServiceImage }}"
- net: host
-{{- if .Mounts }}
- binds:
-{{- range .Mounts }}
- - {{ . }}:{{ . }}
-{{- end }}
-{{- end }}
- env:
- - CORE_IMAGE=core-minimal
- - CORE_GPU={{ if .GPU }}1{{ else }}0{{ end }}
- command:
- - /bin/sh
- - -lc
- - '{{ .EntrypointCommand }}'
-
-files:
- - path: /etc/motd
- contents: |
- core-minimal
- Version: {{ .Version }}
- {{ .Description }}
-
-trust:
- org:
- - linuxkit
- - library
diff --git a/pkg/build/images/core-ml.yml b/pkg/build/images/core-ml.yml
deleted file mode 100644
index bd8bd15..0000000
--- a/pkg/build/images/core-ml.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-# core-ml LinuxKit image template
-kernel:
- image: linuxkit/kernel:6.6.13
- cmdline: "console=tty0 console=ttyS0"
-
-init:
- - linuxkit/init:v1.2.0
- - linuxkit/runc:v1.1.12
- - linuxkit/containerd:v1.7.13
- - linuxkit/ca-certificates:v1.0.0
-
-services:
- - name: core-ml
- image: "{{ .ServiceImage }}"
- net: host
- capabilities:
- - all
-{{- if .Mounts }}
- binds:
-{{- range .Mounts }}
- - {{ . }}:{{ . }}
-{{- end }}
-{{- end }}
- env:
- - CORE_IMAGE=core-ml
- - CORE_GPU={{ if .GPU }}1{{ else }}0{{ end }}
- command:
- - /bin/sh
- - -lc
- - '{{ .EntrypointCommand }}'
-
-files:
- - path: /etc/motd
- contents: |
- core-ml
- Version: {{ .Version }}
- {{ .Description }}
-
-trust:
- org:
- - linuxkit
- - library
diff --git a/pkg/build/installers.go b/pkg/build/installers.go
deleted file mode 100644
index 74ea830..0000000
--- a/pkg/build/installers.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package build
-
-import (
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- buildinstallers "dappco.re/go/build/pkg/build/installers"
-)
-
-// InstallerVariant identifies an installer script profile.
-type InstallerVariant = buildinstallers.InstallerVariant
-
-const (
- // VariantFull generates setup.sh — full installer with PATH setup and shell completions.
- VariantFull InstallerVariant = buildinstallers.VariantFull
- // VariantCI generates ci.sh — minimal download-only installer for CI environments.
- VariantCI InstallerVariant = buildinstallers.VariantCI
- // VariantPHP generates php.sh — installs core CLI + FrankenPHP + Composer.
- VariantPHP InstallerVariant = buildinstallers.VariantPHP
- // VariantGo generates go.sh — installs core CLI + Go toolchain + gopls.
- VariantGo InstallerVariant = buildinstallers.VariantGo
- // VariantAgent generates agent.sh — installs core CLI + core-agent + Claude Code.
- VariantAgent InstallerVariant = buildinstallers.VariantAgent
- // VariantAgentic is the RFC-documented alias for the AI agent installer variant.
- VariantAgentic InstallerVariant = buildinstallers.VariantAgentic
- // VariantDev generates dev.sh — installs core CLI + pulls the core-dev LinuxKit image.
- VariantDev InstallerVariant = buildinstallers.VariantDev
-)
-
-// GenerateInstallerScript renders a single installer script variant from the
-// release version and repository.
-//
-// script, err := build.GenerateInstallerScript(build.VariantCI, "v1.2.3", "dappcore/core")
-// // script starts with the ci.sh template rendered for core binaries
-func GenerateInstallerScript(variant InstallerVariant, version, repo string) core.Result {
- return buildinstallers.GenerateInstaller(variant, installerConfig(version, repo))
-}
-
-// GenerateInstaller is the backwards-compatible alias for GenerateInstallerScript.
-func GenerateInstaller(variant InstallerVariant, version, repo string) core.Result {
- return GenerateInstallerScript(variant, version, repo)
-}
-
-// GenerateAllInstallerScripts renders every installer script variant from the
-// release version and repository.
-//
-// scripts, err := build.GenerateAllInstallerScripts("v1.2.3", "dappcore/core")
-// // scripts["setup.sh"], scripts["ci.sh"], scripts["go.sh"], ...
-func GenerateAllInstallerScripts(version, repo string) core.Result {
- return buildinstallers.GenerateAll(installerConfig(version, repo))
-}
-
-// GenerateAll is the backwards-compatible alias for GenerateAllInstallerScripts.
-func GenerateAll(version, repo string) core.Result {
- return GenerateAllInstallerScripts(version, repo)
-}
-
-// InstallerVariants returns the supported variants in stable output order.
-func InstallerVariants() []InstallerVariant {
- return buildinstallers.Variants()
-}
-
-// InstallerOutputName returns the filename emitted for a variant.
-func InstallerOutputName(variant InstallerVariant) string {
- return buildinstallers.OutputName(variant)
-}
-
-func installerConfig(version, repo string) buildinstallers.InstallerConfig {
- repo = core.Trim(repo)
- binaryName := ""
- if repo != "" {
- binaryName = core.TrimSuffix(ax.Base(repo), ".git")
- if binaryName == "" {
- binaryName = repo
- }
- }
-
- return buildinstallers.InstallerConfig{
- Version: core.Trim(version),
- Repo: repo,
- BinaryName: binaryName,
- }
-}
diff --git a/pkg/build/installers/installer.go b/pkg/build/installers/installer.go
deleted file mode 100644
index 17a6906..0000000
--- a/pkg/build/installers/installer.go
+++ /dev/null
@@ -1,283 +0,0 @@
-// Package installers generates installer shell scripts for Core CLI releases.
-// Each variant targets a specific install profile (full, CI, PHP, Go, agent, dev).
-package installers
-
-import (
- "embed" // Note: AX-6 — embeds installer templates into the package.
- "regexp" // Note: AX-6 — validates release versions with a precompiled pattern.
- "text/template" // Note: AX-6 — renders shell installer templates.
-
- "dappco.re/go" // Note: AX-6 — provides approved string helpers and template writer construction.
-)
-
-//go:embed templates/*.tmpl
-var installerTemplates embed.FS
-
-var safeInstallerVersion = regexp.MustCompile(`^[A-Za-z0-9._+-]+$`)
-
-// DefaultScriptBaseURL is the RFC-documented CDN origin for generated
-// installer scripts.
-const DefaultScriptBaseURL = "https://lthn.sh"
-
-// InstallerVariant represents an installer script variant.
-//
-// var v installers.InstallerVariant = installers.VariantFull
-type InstallerVariant string
-
-const (
- // VariantFull generates setup.sh — full installer with PATH setup and shell completions.
- VariantFull InstallerVariant = "full"
- // VariantCI generates ci.sh — minimal download-only installer for CI environments.
- VariantCI InstallerVariant = "ci"
- // VariantPHP generates php.sh — installs core CLI + FrankenPHP + Composer (~50MB).
- VariantPHP InstallerVariant = "php"
- // VariantGo generates go.sh — installs core CLI + Go toolchain + gopls (~200MB).
- VariantGo InstallerVariant = "go"
- // VariantAgent generates agent.sh — installs core CLI + core-agent + Claude Code (~30MB).
- VariantAgent InstallerVariant = "agent"
- // VariantAgentic is the RFC-documented alias for the AI agent installer variant.
- VariantAgentic InstallerVariant = VariantAgent
- // VariantDev generates dev.sh — installs core CLI + pulls core-dev LinuxKit image (~500MB).
- VariantDev InstallerVariant = "dev"
-)
-
-var installerVariantOrder = []InstallerVariant{
- VariantFull,
- VariantCI,
- VariantPHP,
- VariantGo,
- VariantAgent,
- VariantDev,
-}
-
-// variantTemplates maps each InstallerVariant to its embedded template filename and output script name.
-var variantTemplates = map[InstallerVariant]struct {
- tmpl string
- output string
-}{
- VariantFull: {tmpl: "templates/setup.sh.tmpl", output: "setup.sh"},
- VariantCI: {tmpl: "templates/ci.sh.tmpl", output: "ci.sh"},
- VariantPHP: {tmpl: "templates/php.sh.tmpl", output: "php.sh"},
- VariantGo: {tmpl: "templates/go.sh.tmpl", output: "go.sh"},
- VariantAgent: {tmpl: "templates/agent.sh.tmpl", output: "agent.sh"},
- VariantDev: {tmpl: "templates/dev.sh.tmpl", output: "dev.sh"},
-}
-
-// Variants returns the supported installer variants in stable output order.
-func Variants() []InstallerVariant {
- return append([]InstallerVariant(nil), installerVariantOrder...)
-}
-
-// OutputName returns the generated script filename for a variant.
-func OutputName(variant InstallerVariant) string {
- entry, ok := variantTemplates[canonicalVariant(variant)]
- if !ok {
- return ""
- }
- return entry.output
-}
-
-// InstallerConfig holds the values injected into installer script templates.
-//
-// cfg := installers.InstallerConfig{
-// Version: "v1.2.3",
-// Repo: "dappcore/core",
-// BinaryName: "core",
-// }
-type InstallerConfig struct {
- // Version is the release tag (e.g. "v1.2.3").
- Version string
- // Repo is the GitHub repository in "owner/name" format (e.g. "dappcore/core").
- Repo string
- // BinaryName is the name of the installed binary (e.g. "core").
- BinaryName string
- // ScriptBaseURL is the public base URL that hosts the generated installer scripts.
- // Empty values default to the RFC CDN origin.
- ScriptBaseURL string
-}
-
-// GenerateInstaller renders an installer script for the given variant.
-//
-// // RFC-shaped form:
-// script, err := installers.GenerateInstaller(installers.VariantCI, "v1.2.3", "dappcore/core")
-//
-// // Rich form with explicit binary name and script host:
-// script, err := installers.GenerateInstaller(installers.VariantCI, installers.InstallerConfig{
-// Version: "v1.2.3", Repo: "dappcore/core", BinaryName: "core",
-// })
-func GenerateInstaller(variant InstallerVariant, args ...any) core.Result {
- cfgResult := normalizeInstallerArgs(args...)
- if !cfgResult.OK {
- return cfgResult
- }
- cfg := cfgResult.Value.(InstallerConfig)
-
- variant = canonicalVariant(variant)
- valid := validateInstallerVersion(cfg.Version)
- if !valid.OK {
- return core.Fail(core.E("installers.GenerateInstaller", "version is not a safe release identifier", core.NewError(valid.Error())))
- }
-
- entry, ok := variantTemplates[variant]
- if !ok {
- return core.Fail(core.E("installers.GenerateInstaller", "unknown variant: "+string(variant), nil))
- }
-
- raw, err := installerTemplates.ReadFile(entry.tmpl)
- if err != nil {
- return core.Fail(core.E("installers.GenerateInstaller", "failed to read template "+entry.tmpl, err))
- }
-
- tmpl, err := template.New(entry.output).Funcs(template.FuncMap{
- "shellQuote": shellQuote,
- }).Parse(string(raw))
- if err != nil {
- return core.Fail(core.E("installers.GenerateInstaller", "failed to parse template "+entry.tmpl, err))
- }
-
- // Note: AX-6 — core.NewBuffer is unavailable in the pinned core module;
- // core.NewBuilder is the available Core-owned writer.
- buf := core.NewBuilder()
- if err := tmpl.Execute(buf, cfg); err != nil {
- return core.Fail(core.E("installers.GenerateInstaller", "failed to render template "+entry.tmpl, err))
- }
-
- return core.Ok(buf.String())
-}
-
-// GenerateAll renders all installer variants and returns a map of output filename → script content.
-//
-// // RFC-shaped form:
-// scripts, err := installers.GenerateAll("v1.2.3", "dappcore/core")
-//
-// // Rich form with explicit binary name and script host:
-// scripts, err := installers.GenerateAll(installers.InstallerConfig{
-// Version: "v1.2.3", Repo: "dappcore/core", BinaryName: "core",
-// })
-// for name, content := range scripts {
-// // name: "setup.sh", content: "#!/usr/bin/env bash\n..."
-// }
-func GenerateAll(args ...any) core.Result {
- cfgResult := normalizeInstallerArgs(args...)
- if !cfgResult.OK {
- return cfgResult
- }
- cfg := cfgResult.Value.(InstallerConfig)
-
- valid := validateInstallerVersion(cfg.Version)
- if !valid.OK {
- return core.Fail(core.E("installers.GenerateAll", "version is not a safe release identifier", core.NewError(valid.Error())))
- }
-
- out := make(map[string]string, len(installerVariantOrder))
-
- for _, variant := range installerVariantOrder {
- entry := variantTemplates[variant]
- script := GenerateInstaller(variant, cfg)
- if !script.OK {
- return core.Fail(core.E("installers.GenerateAll", "failed to generate variant "+string(variant), core.NewError(script.Error())))
- }
- out[entry.output] = script.Value.(string)
- }
-
- return core.Ok(out)
-}
-
-func normalizeInstallerArgs(args ...any) core.Result {
- switch len(args) {
- case 1:
- switch cfg := args[0].(type) {
- case InstallerConfig:
- return core.Ok(normalizeInstallerConfig(cfg))
- case *InstallerConfig:
- if cfg == nil {
- return core.Ok(normalizeInstallerConfig(InstallerConfig{}))
- }
- return core.Ok(normalizeInstallerConfig(*cfg))
- default:
- return core.Fail(core.E("installers.normalizeInstallerArgs", "expected InstallerConfig or *InstallerConfig", nil))
- }
- case 2:
- version, ok := args[0].(string)
- if !ok {
- return core.Fail(core.E("installers.normalizeInstallerArgs", "version must be a string", nil))
- }
- repo, ok := args[1].(string)
- if !ok {
- return core.Fail(core.E("installers.normalizeInstallerArgs", "repo must be a string", nil))
- }
- return core.Ok(normalizeInstallerConfig(InstallerConfig{
- Version: version,
- Repo: repo,
- BinaryName: defaultInstallerBinaryName(repo),
- }))
- default:
- return core.Fail(core.E("installers.normalizeInstallerArgs", "expected either InstallerConfig or version/repo arguments", nil))
- }
-}
-
-func normalizeInstallerConfig(cfg InstallerConfig) InstallerConfig {
- baseURL := trimTrailingSlashes(core.Trim(cfg.ScriptBaseURL))
- if baseURL == "" {
- baseURL = DefaultScriptBaseURL
- }
- cfg.ScriptBaseURL = baseURL
- if core.Trim(cfg.BinaryName) == "" {
- cfg.BinaryName = defaultInstallerBinaryName(cfg.Repo)
- }
- return cfg
-}
-
-func defaultInstallerBinaryName(repo string) string {
- repo = core.Trim(repo)
- if repo == "" {
- return ""
- }
-
- parts := core.Split(core.Replace(repo, "\\", "/"), "/")
- for i := len(parts) - 1; i >= 0; i-- {
- if parts[i] != "" {
- return parts[i]
- }
- }
-
- return ""
-}
-
-func shellQuote(value string) string {
- if value == "" {
- return "''"
- }
-
- return "'" + core.Replace(value, "'", `'"'"'`) + "'"
-}
-
-func canonicalVariant(variant InstallerVariant) InstallerVariant {
- normalized := InstallerVariant(core.Lower(core.Trim(string(variant))))
- if normalized == "agentic" {
- return VariantAgent
- }
- return normalized
-}
-
-func validateInstallerVersion(version string) core.Result {
- trimmed := core.Trim(version)
- if trimmed == "" {
- return core.Ok(nil)
- }
- if version != trimmed {
- return core.Fail(core.E("installers.validateInstallerVersion", "version contains unsupported whitespace", nil))
- }
- if !safeInstallerVersion.MatchString(version) {
- return core.Fail(core.E("installers.validateInstallerVersion", "version contains unsupported characters", nil))
- }
-
- return core.Ok(nil)
-}
-
-func trimTrailingSlashes(value string) string {
- for core.HasSuffix(value, "/") {
- value = core.TrimSuffix(value, "/")
- }
- return value
-}
diff --git a/pkg/build/installers/installer_example_test.go b/pkg/build/installers/installer_example_test.go
deleted file mode 100644
index fe28e9c..0000000
--- a/pkg/build/installers/installer_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package installers
-
-import core "dappco.re/go"
-
-// ExampleVariants references Variants on this package API surface.
-func ExampleVariants() {
- _ = Variants
- core.Println("Variants")
- // Output: Variants
-}
-
-// ExampleOutputName references OutputName on this package API surface.
-func ExampleOutputName() {
- _ = OutputName
- core.Println("OutputName")
- // Output: OutputName
-}
-
-// ExampleGenerateInstaller references GenerateInstaller on this package API surface.
-func ExampleGenerateInstaller() {
- _ = GenerateInstaller
- core.Println("GenerateInstaller")
- // Output: GenerateInstaller
-}
-
-// ExampleGenerateAll references GenerateAll on this package API surface.
-func ExampleGenerateAll() {
- _ = GenerateAll
- core.Println("GenerateAll")
- // Output: GenerateAll
-}
diff --git a/pkg/build/installers/installer_test.go b/pkg/build/installers/installer_test.go
deleted file mode 100644
index 97b9759..0000000
--- a/pkg/build/installers/installer_test.go
+++ /dev/null
@@ -1,396 +0,0 @@
-package installers
-
-import (
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/testassert"
-)
-
-// validConfig is a fully populated InstallerConfig used as the happy-path baseline.
-var validConfig = InstallerConfig{
- Version: "v1.2.3",
- Repo: "dappcore/core",
- BinaryName: "core",
-}
-
-func requireGeneratedInstaller(t *testing.T, variant InstallerVariant, cfg InstallerConfig) string {
- t.Helper()
- result := GenerateInstaller(variant, cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func requireGeneratedInstallers(t *testing.T, cfg InstallerConfig) map[string]string {
- t.Helper()
- result := GenerateAll(cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(map[string]string)
-}
-
-// TestInstaller_GenerateInstaller_Good verifies that each known variant produces a non-empty
-// shell script containing the expected shebang, binary name, version, and repo strings.
-func TestInstaller_GenerateInstaller_Good(t *testing.T) {
- allVariants := []InstallerVariant{
- VariantFull,
- VariantCI,
- VariantPHP,
- VariantGo,
- VariantAgent,
- VariantDev,
- }
-
- for _, variant := range allVariants {
- v := variant // capture range variable
- t.Run(string(v), func(t *testing.T) {
- result := GenerateInstaller(v, validConfig)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- script := result.Value.(string)
- if stdlibAssertEmpty(script) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertContains(script, "#!/usr/bin/env bash") {
- t.Fatal("script must start with bash shebang")
- }
- if !stdlibAssertContains(script, validConfig.BinaryName) {
- t.Fatal("script must reference binary name")
- }
- if !stdlibAssertContains(script, validConfig.Version) {
- t.Fatal("script must reference version")
- }
- if !stdlibAssertContains(script, validConfig.Repo) {
- t.Fatal("script must reference repo")
- }
- if !stdlibAssertContains(script, DefaultScriptBaseURL) {
- t.Fatal("script must reference the RFC installer host")
- }
-
- })
- }
-}
-
-func TestInstaller_GenerateInstaller_CustomScriptBaseURL_Good(t *testing.T) {
- result := GenerateInstaller(VariantFull, InstallerConfig{
- Version: "v1.2.3",
- Repo: "dappcore/core",
- BinaryName: "core",
- ScriptBaseURL: "https://downloads.example.com/",
- })
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- script := result.Value.(string)
- if !stdlibAssertContains(script, "https://downloads.example.com/setup.sh") {
- t.Fatalf("expected %v to contain %v", script, "https://downloads.example.com/setup.sh")
- }
- if stdlibAssertContains(script, "https://downloads.example.com//setup.sh") {
- t.Fatalf("expected %v not to contain %v", script, "https://downloads.example.com//setup.sh")
- }
-
-}
-
-func TestInstaller_GenerateInstaller_AgenticAlias_Good(t *testing.T) {
- result := GenerateInstaller("agentic", validConfig)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- script := result.Value.(string)
- if stdlibAssertEmpty(script) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertContains(script, DefaultScriptBaseURL) {
- t.Fatalf("expected %v to contain %v", script, DefaultScriptBaseURL)
- }
-
-}
-
-func TestInstaller_GenerateInstaller_DevVariantUsesVersionedImage_Good(t *testing.T) {
- result := GenerateInstaller(VariantDev, validConfig)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- script := result.Value.(string)
- if !stdlibAssertContains(script, `DEV_IMAGE_VERSION="${VERSION#v}"`) {
- t.Fatalf("expected %v to contain %v", script, `DEV_IMAGE_VERSION="${VERSION#v}"`)
- }
- if !stdlibAssertContains(script, `DEV_IMAGE="ghcr.io/dappcore/core-dev:${DEV_IMAGE_VERSION}"`) {
-
- // TestInstaller_GenerateInstaller_Bad verifies that an unknown variant returns an error and empty output.
- t.Fatalf("expected %v to contain %v", script, `DEV_IMAGE="ghcr.io/dappcore/core-dev:${DEV_IMAGE_VERSION}"`)
- }
- if stdlibAssertContains(script, "core-dev:latest") {
- t.Fatalf("expected %v not to contain %v", script, "core-dev:latest")
- }
-
-}
-
-func TestInstaller_GenerateInstaller_Bad(t *testing.T) {
- t.Run("unknown variant returns error", func(t *testing.T) {
- result := GenerateInstaller("nonexistent", validConfig)
- if result.OK {
- t.Fatal("expected error")
- }
-
- })
-
- t.Run("empty variant string returns error", func(t *testing.T) {
- result := GenerateInstaller("", validConfig)
- if result.OK {
- t.Fatal("expected error")
- }
-
- })
-
- t.Run("unsafe version returns error", func(t *testing.T) {
- result := GenerateInstaller(VariantCI, InstallerConfig{
- Version: "v1.2.3\n--flag",
- Repo: "dappcore/core",
- BinaryName: "core",
- })
- if result.OK {
- t.Fatal("expected error")
- }
-
- })
-
- t.Run("version with spaces returns error", func(t *testing.T) {
- result := GenerateInstaller(VariantCI, InstallerConfig{
- Version: " v1.2.3 ",
- Repo: "dappcore/core",
- BinaryName: "core",
- })
- if result.OK {
- t.Fatal("expected error")
- }
- })
-}
-
-// TestInstaller_GenerateInstaller_Ugly verifies that empty config fields are rendered without
-// panicking — the template may produce incomplete scripts but must not error.
-func TestInstaller_GenerateInstaller_Ugly(t *testing.T) {
- t.Run("empty Version renders without error", func(t *testing.T) {
- cfg := InstallerConfig{Version: "", Repo: "dappcore/core", BinaryName: "core"}
- result := GenerateInstaller(VariantFull, cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- script := result.Value.(string)
- if stdlibAssertEmpty(script) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("empty Repo renders without error", func(t *testing.T) {
- cfg := InstallerConfig{Version: "v1.0.0", Repo: "", BinaryName: "core"}
- result := GenerateInstaller(VariantCI, cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- script := result.Value.(string)
- if stdlibAssertEmpty(script) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("empty BinaryName renders without error", func(t *testing.T) {
- cfg := InstallerConfig{Version: "v1.0.0", Repo: "dappcore/core", BinaryName: ""}
- result := GenerateInstaller(VariantAgent, cfg)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- script := result.Value.(string)
- if stdlibAssertEmpty(script) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("fully empty config renders without error", func(t *testing.T) {
- result := GenerateInstaller(VariantDev, InstallerConfig{})
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- script := result.Value.(string)
- if stdlibAssertEmpty(script) {
- t.Fatal("expected non-empty")
- }
-
- })
-}
-
-func TestInstaller_GenerateInstaller_QuotesValues(t *testing.T) {
- cfg := InstallerConfig{
- Version: "v1.2.3-beta+1",
- Repo: "dappcore/core",
- BinaryName: "core's tool",
- }
-
- script := requireGeneratedInstaller(t, VariantCI, cfg)
- if !stdlibAssertContains(script, "BINARY_NAME='core'\"'\"'s tool'") {
- t.Fatalf("expected %v to contain %v", script, "BINARY_NAME='core'\"'\"'s tool'")
- }
- if !stdlibAssertContains(script, "VERSION='v1.2.3-beta+1'") {
- t.Fatalf("expected %v to contain %v", script, "VERSION='v1.2.3-beta+1'")
- }
- if !stdlibAssertContains(script, "REPO='dappcore/core'") {
- t.Fatalf("expected %v to contain %v", script, "REPO='dappcore/core'")
- }
-
-}
-
-func TestInstaller_GenerateAll_Good(t *testing.T) {
- scripts := requireGeneratedInstallers(t, validConfig)
-
- expectedOutputs := []string{
- "setup.sh",
- "ci.sh",
- "php.sh",
- "go.sh",
- "agent.sh",
- "dev.sh",
- }
- if len(scripts) != len(variantTemplates) {
- t.Fatal("GenerateAll must return one entry per variant")
- }
-
- for _, name := range expectedOutputs {
- t.Run(name, func(t *testing.T) {
- content, ok := scripts[name]
- if !(ok) {
- t.Fatalf("GenerateAll must include %s", name)
- }
- if stdlibAssertEmpty(content) {
- t.Fatal("expected non-empty")
- }
- if !stdlibAssertContains(content, "#!/usr/bin/env bash") {
- t.Fatalf("expected %v to contain %v", content, "#!/usr/bin/env bash")
- }
- if !stdlibAssertContains(content, validConfig.BinaryName) {
- t.Fatalf("expected %v to contain %v", content, validConfig.BinaryName)
- }
- if !stdlibAssertContains(content, DefaultScriptBaseURL) {
- t.Fatalf("expected %v to contain %v", content, DefaultScriptBaseURL)
- }
-
- })
- }
-}
-
-func TestInstaller_Variants_Good(t *testing.T) {
- if !stdlibAssertEqual([]InstallerVariant{VariantFull, VariantCI, VariantPHP, VariantGo, VariantAgent, VariantDev}, Variants()) {
- t.Fatalf("want %v, got %v", []InstallerVariant{VariantFull, VariantCI, VariantPHP, VariantGo, VariantAgent, VariantDev}, Variants())
- }
-
-}
-
-func TestInstaller_GenerateAll_Bad_UnsafeVersion(t *testing.T) {
- result := GenerateAll(InstallerConfig{
- Version: "v1.2.3 && echo unsafe",
- Repo: "dappcore/core",
- BinaryName: "core",
- })
- if result.OK {
- t.Fatal("expected error")
- }
-
-}
-
-func TestInstaller_OutputName_Good(t *testing.T) {
- if !stdlibAssertEqual("setup.sh", OutputName(VariantFull)) {
- t.Fatalf("want %v, got %v", "setup.sh", OutputName(VariantFull))
- }
- if !stdlibAssertEqual("ci.sh", OutputName(VariantCI)) {
- t.Fatalf("want %v, got %v", "ci.sh", OutputName(VariantCI))
- }
- if !stdlibAssertEqual("php.sh", OutputName(VariantPHP)) {
- t.Fatalf("want %v, got %v", "php.sh", OutputName(VariantPHP))
- }
- if !stdlibAssertEqual("go.sh", OutputName(VariantGo)) {
- t.Fatalf("want %v, got %v", "go.sh", OutputName(VariantGo))
- }
- if !stdlibAssertEqual("agent.sh", OutputName(VariantAgent)) {
- t.Fatalf("want %v, got %v", "agent.sh", OutputName(VariantAgent))
- }
- if !stdlibAssertEqual("agent.sh", OutputName("agentic")) {
- t.Fatalf("want %v, got %v", "agent.sh", OutputName("agentic"))
- }
- if !stdlibAssertEqual("dev.sh", OutputName(VariantDev)) {
- t.Fatalf("want %v, got %v", "dev.sh", OutputName(VariantDev))
- }
- if !stdlibAssertEmpty(OutputName("bogus")) {
- t.Fatalf("expected empty, got %v", OutputName("bogus"))
- }
-
-}
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
-
-// --- v0.9.0 generated compliance triplets ---
-func TestInstaller_Variants_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Variants()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestInstaller_Variants_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Variants()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestInstaller_OutputName_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = OutputName(InstallerVariant("linux"))
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestInstaller_OutputName_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = OutputName(InstallerVariant("linux"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestInstaller_GenerateAll_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = GenerateAll()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestInstaller_GenerateAll_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = GenerateAll()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/installers/templates/agent.sh.tmpl b/pkg/build/installers/templates/agent.sh.tmpl
deleted file mode 100644
index 2170229..0000000
--- a/pkg/build/installers/templates/agent.sh.tmpl
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/env bash
-# agent.sh — Agent variant installer for {{.BinaryName}} {{.Version}}
-# Installs: core CLI + core-agent + Claude Code
-#
-# Usage:
-# curl -sL {{.ScriptBaseURL}}/agent.sh | bash
-set -euo pipefail
-
-BINARY_NAME={{ shellQuote .BinaryName }}
-VERSION={{ shellQuote .Version }}
-REPO={{ shellQuote .Repo }}
-GITHUB_BASE="https://github.com/${REPO}"
-
-detect_os() {
- case "$(uname -s)" in
- Linux*) echo "linux" ;;
- Darwin*) echo "darwin" ;;
- *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
- esac
-}
-
-detect_arch() {
- case "$(uname -m)" in
- x86_64) echo "amd64" ;;
- aarch64|arm64) echo "arm64" ;;
- *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
- esac
-}
-
-OS="$(detect_os)"
-ARCH="$(detect_arch)"
-
-TMP_DIR="$(mktemp -d)"
-trap 'rm -rf "${TMP_DIR}"' EXIT
-
-INSTALL_DIR="/usr/local/bin"
-USE_SUDO="sudo"
-if [ -w "${INSTALL_DIR}" ]; then USE_SUDO=""; fi
-
-# ── Install core CLI ──────────────────────────────────────────────────────────
-
-TARBALL="${BINARY_NAME}_${OS}_${ARCH}.tar.gz"
-echo "Downloading ${BINARY_NAME} ${VERSION}..."
-curl -fsSL "${GITHUB_BASE}/releases/download/${VERSION}/${TARBALL}" -o "${TMP_DIR}/${TARBALL}"
-tar -xzf "${TMP_DIR}/${TARBALL}" -C "${TMP_DIR}"
-${USE_SUDO} install -m 0755 "${TMP_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
-echo "Installed ${BINARY_NAME} ${VERSION}"
-
-# ── Install core-agent ────────────────────────────────────────────────────────
-
-if ! command -v core-agent &>/dev/null; then
- echo "Installing core-agent..."
- AGENT_TARBALL="core-agent_${OS}_${ARCH}.tar.gz"
- AGENT_URL="${GITHUB_BASE}/releases/download/${VERSION}/${AGENT_TARBALL}"
- curl -fsSL "${AGENT_URL}" -o "${TMP_DIR}/${AGENT_TARBALL}" 2>/dev/null || {
- echo "core-agent not bundled in this release, skipping."
- }
- if [ -f "${TMP_DIR}/${AGENT_TARBALL}" ]; then
- tar -xzf "${TMP_DIR}/${AGENT_TARBALL}" -C "${TMP_DIR}"
- ${USE_SUDO} install -m 0755 "${TMP_DIR}/core-agent" "${INSTALL_DIR}/core-agent"
- echo "Installed core-agent"
- fi
-else
- echo "core-agent already installed"
-fi
-
-# ── Install Claude Code ───────────────────────────────────────────────────────
-
-if ! command -v claude &>/dev/null; then
- if command -v npm &>/dev/null; then
- echo "Installing Claude Code..."
- npm install -g @anthropic-ai/claude-code
- echo "Installed Claude Code"
- else
- echo "npm not found — skipping Claude Code installation. Install Node.js and run: npm install -g @anthropic-ai/claude-code"
- fi
-else
- echo "Claude Code already installed: $(claude --version 2>/dev/null | head -1)"
-fi
-
-# ── Verify ────────────────────────────────────────────────────────────────────
-
-echo ""
-"${BINARY_NAME}" --version
-echo "Agent variant installation complete."
diff --git a/pkg/build/installers/templates/ci.sh.tmpl b/pkg/build/installers/templates/ci.sh.tmpl
deleted file mode 100644
index 1df3cee..0000000
--- a/pkg/build/installers/templates/ci.sh.tmpl
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env bash
-# ci.sh — Minimal CI installer for {{.BinaryName}} {{.Version}}
-# Downloads the binary only. No PATH modification. No interactive prompts.
-# Adds to GITHUB_PATH when running inside GitHub Actions.
-#
-# Usage:
-# curl -sL {{.ScriptBaseURL}}/ci.sh | bash
-set -euo pipefail
-
-BINARY_NAME={{ shellQuote .BinaryName }}
-VERSION={{ shellQuote .Version }}
-REPO={{ shellQuote .Repo }}
-GITHUB_BASE="https://github.com/${REPO}"
-
-# ── OS / ARCH detection ──────────────────────────────────────────────────────
-
-detect_os() {
- case "$(uname -s)" in
- Linux*) echo "linux" ;;
- Darwin*) echo "darwin" ;;
- *)
- echo "Unsupported OS: $(uname -s)" >&2
- exit 1
- ;;
- esac
-}
-
-detect_arch() {
- case "$(uname -m)" in
- x86_64) echo "amd64" ;;
- aarch64|arm64) echo "arm64" ;;
- *)
- echo "Unsupported architecture: $(uname -m)" >&2
- exit 1
- ;;
- esac
-}
-
-OS="$(detect_os)"
-ARCH="$(detect_arch)"
-
-TARBALL="${BINARY_NAME}_${OS}_${ARCH}.tar.gz"
-DOWNLOAD_URL="${GITHUB_BASE}/releases/download/${VERSION}/${TARBALL}"
-
-# ── Download & extract ────────────────────────────────────────────────────────
-
-TMP_DIR="$(mktemp -d)"
-trap 'rm -rf "${TMP_DIR}"' EXIT
-
-echo "Downloading ${BINARY_NAME} ${VERSION} (${OS}/${ARCH})..."
-curl -fsSL "${DOWNLOAD_URL}" -o "${TMP_DIR}/${TARBALL}"
-
-tar -xzf "${TMP_DIR}/${TARBALL}" -C "${TMP_DIR}"
-
-# ── Install to runner tool cache or temp ─────────────────────────────────────
-
-INSTALL_DIR="${RUNNER_TOOL_CACHE:-/usr/local/bin}"
-if [ ! -w "${INSTALL_DIR}" ]; then
- INSTALL_DIR="${HOME}/.local/bin"
- mkdir -p "${INSTALL_DIR}"
-fi
-
-install -m 0755 "${TMP_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
-echo "Installed ${BINARY_NAME} to ${INSTALL_DIR}/${BINARY_NAME}"
-
-# ── GITHUB_PATH registration ──────────────────────────────────────────────────
-
-if [ -n "${GITHUB_PATH:-}" ]; then
- echo "${INSTALL_DIR}" >> "${GITHUB_PATH}"
- echo "Registered ${INSTALL_DIR} in GITHUB_PATH"
-fi
-
-echo "${BINARY_NAME} ${VERSION} ready."
diff --git a/pkg/build/installers/templates/dev.sh.tmpl b/pkg/build/installers/templates/dev.sh.tmpl
deleted file mode 100644
index a2a5331..0000000
--- a/pkg/build/installers/templates/dev.sh.tmpl
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/usr/bin/env bash
-# dev.sh — Dev variant installer for {{.BinaryName}} {{.Version}}
-# Installs: core CLI + pulls core-dev LinuxKit Docker image (~500MB)
-#
-# Usage:
-# curl -sL {{.ScriptBaseURL}}/dev.sh | bash
-set -euo pipefail
-
-BINARY_NAME={{ shellQuote .BinaryName }}
-VERSION={{ shellQuote .Version }}
-REPO={{ shellQuote .Repo }}
-GITHUB_BASE="https://github.com/${REPO}"
-DEV_IMAGE_VERSION="${VERSION#v}"
-if [ -z "${DEV_IMAGE_VERSION}" ]; then
- DEV_IMAGE_VERSION="latest"
-fi
-DEV_IMAGE="ghcr.io/dappcore/core-dev:${DEV_IMAGE_VERSION}"
-
-detect_os() {
- case "$(uname -s)" in
- Linux*) echo "linux" ;;
- Darwin*) echo "darwin" ;;
- *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
- esac
-}
-
-detect_arch() {
- case "$(uname -m)" in
- x86_64) echo "amd64" ;;
- aarch64|arm64) echo "arm64" ;;
- *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
- esac
-}
-
-OS="$(detect_os)"
-ARCH="$(detect_arch)"
-
-TMP_DIR="$(mktemp -d)"
-trap 'rm -rf "${TMP_DIR}"' EXIT
-
-INSTALL_DIR="/usr/local/bin"
-USE_SUDO="sudo"
-if [ -w "${INSTALL_DIR}" ]; then USE_SUDO=""; fi
-
-# ── Install core CLI ──────────────────────────────────────────────────────────
-
-TARBALL="${BINARY_NAME}_${OS}_${ARCH}.tar.gz"
-echo "Downloading ${BINARY_NAME} ${VERSION}..."
-curl -fsSL "${GITHUB_BASE}/releases/download/${VERSION}/${TARBALL}" -o "${TMP_DIR}/${TARBALL}"
-tar -xzf "${TMP_DIR}/${TARBALL}" -C "${TMP_DIR}"
-${USE_SUDO} install -m 0755 "${TMP_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
-echo "Installed ${BINARY_NAME} ${VERSION}"
-
-# ── Pull core-dev Docker image ────────────────────────────────────────────────
-
-if ! command -v docker &>/dev/null; then
- echo "Docker not found — skipping core-dev image pull."
- echo "Install Docker and run: docker pull ${DEV_IMAGE}"
-else
- echo "Pulling core-dev image (this may take a while, ~500MB)..."
- docker pull "${DEV_IMAGE}"
- echo "Pulled ${DEV_IMAGE}"
-fi
-
-# ── Verify ────────────────────────────────────────────────────────────────────
-
-echo ""
-"${BINARY_NAME}" --version
-echo "Dev variant installation complete."
diff --git a/pkg/build/installers/templates/go.sh.tmpl b/pkg/build/installers/templates/go.sh.tmpl
deleted file mode 100644
index 72ba716..0000000
--- a/pkg/build/installers/templates/go.sh.tmpl
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env bash
-# go.sh — Go variant installer for {{.BinaryName}} {{.Version}}
-# Installs: core CLI + Go toolchain (if missing) + gopls
-#
-# Usage:
-# curl -sL {{.ScriptBaseURL}}/go.sh | bash
-set -euo pipefail
-
-BINARY_NAME={{ shellQuote .BinaryName }}
-VERSION={{ shellQuote .Version }}
-REPO={{ shellQuote .Repo }}
-GITHUB_BASE="https://github.com/${REPO}"
-GO_VERSION="1.24.1"
-
-detect_os() {
- case "$(uname -s)" in
- Linux*) echo "linux" ;;
- Darwin*) echo "darwin" ;;
- *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
- esac
-}
-
-detect_arch() {
- case "$(uname -m)" in
- x86_64) echo "amd64" ;;
- aarch64|arm64) echo "arm64" ;;
- *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
- esac
-}
-
-OS="$(detect_os)"
-ARCH="$(detect_arch)"
-
-TMP_DIR="$(mktemp -d)"
-trap 'rm -rf "${TMP_DIR}"' EXIT
-
-INSTALL_DIR="/usr/local/bin"
-USE_SUDO="sudo"
-if [ -w "${INSTALL_DIR}" ]; then USE_SUDO=""; fi
-
-# ── Install core CLI ──────────────────────────────────────────────────────────
-
-TARBALL="${BINARY_NAME}_${OS}_${ARCH}.tar.gz"
-echo "Downloading ${BINARY_NAME} ${VERSION}..."
-curl -fsSL "${GITHUB_BASE}/releases/download/${VERSION}/${TARBALL}" -o "${TMP_DIR}/${TARBALL}"
-tar -xzf "${TMP_DIR}/${TARBALL}" -C "${TMP_DIR}"
-${USE_SUDO} install -m 0755 "${TMP_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
-echo "Installed ${BINARY_NAME} ${VERSION}"
-
-# ── Install Go ────────────────────────────────────────────────────────────────
-
-if ! command -v go &>/dev/null; then
- echo "Installing Go ${GO_VERSION}..."
- GO_TARBALL="go${GO_VERSION}.${OS}-${ARCH}.tar.gz"
- curl -fsSL "https://go.dev/dl/${GO_TARBALL}" -o "${TMP_DIR}/${GO_TARBALL}"
- ${USE_SUDO} tar -C /usr/local -xzf "${TMP_DIR}/${GO_TARBALL}"
- # Add to PATH for this session
- export PATH="/usr/local/go/bin:${PATH}"
- echo "Installed Go ${GO_VERSION}"
-else
- echo "Go already installed: $(go version)"
-fi
-
-# ── Install gopls ─────────────────────────────────────────────────────────────
-
-if ! command -v gopls &>/dev/null; then
- echo "Installing gopls..."
- go install golang.org/x/tools/gopls@latest
- echo "Installed gopls"
-else
- echo "gopls already installed"
-fi
-
-# ── Verify ────────────────────────────────────────────────────────────────────
-
-echo ""
-"${BINARY_NAME}" --version
-echo "Go variant installation complete."
diff --git a/pkg/build/installers/templates/php.sh.tmpl b/pkg/build/installers/templates/php.sh.tmpl
deleted file mode 100644
index b618497..0000000
--- a/pkg/build/installers/templates/php.sh.tmpl
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env bash
-# php.sh — PHP variant installer for {{.BinaryName}} {{.Version}}
-# Installs: core CLI + FrankenPHP + Composer
-#
-# Usage:
-# curl -sL {{.ScriptBaseURL}}/php.sh | bash
-set -euo pipefail
-
-BINARY_NAME={{ shellQuote .BinaryName }}
-VERSION={{ shellQuote .Version }}
-REPO={{ shellQuote .Repo }}
-GITHUB_BASE="https://github.com/${REPO}"
-
-detect_os() {
- case "$(uname -s)" in
- Linux*) echo "linux" ;;
- Darwin*) echo "darwin" ;;
- *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
- esac
-}
-
-detect_arch() {
- case "$(uname -m)" in
- x86_64) echo "amd64" ;;
- aarch64|arm64) echo "arm64" ;;
- *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
- esac
-}
-
-OS="$(detect_os)"
-ARCH="$(detect_arch)"
-
-TMP_DIR="$(mktemp -d)"
-trap 'rm -rf "${TMP_DIR}"' EXIT
-
-INSTALL_DIR="/usr/local/bin"
-USE_SUDO="sudo"
-if [ -w "${INSTALL_DIR}" ]; then USE_SUDO=""; fi
-
-# ── Install core CLI ──────────────────────────────────────────────────────────
-
-TARBALL="${BINARY_NAME}_${OS}_${ARCH}.tar.gz"
-echo "Downloading ${BINARY_NAME} ${VERSION}..."
-curl -fsSL "${GITHUB_BASE}/releases/download/${VERSION}/${TARBALL}" -o "${TMP_DIR}/${TARBALL}"
-tar -xzf "${TMP_DIR}/${TARBALL}" -C "${TMP_DIR}"
-${USE_SUDO} install -m 0755 "${TMP_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
-echo "Installed ${BINARY_NAME} ${VERSION}"
-
-# ── Install FrankenPHP ────────────────────────────────────────────────────────
-
-if ! command -v frankenphp &>/dev/null; then
- echo "Installing FrankenPHP..."
- FRANKEN_VERSION="latest"
- FRANKEN_URL="https://github.com/dunglas/frankenphp/releases/${FRANKEN_VERSION}/download/frankenphp-${OS}-${ARCH}"
- curl -fsSL "${FRANKEN_URL}" -o "${TMP_DIR}/frankenphp"
- ${USE_SUDO} install -m 0755 "${TMP_DIR}/frankenphp" "${INSTALL_DIR}/frankenphp"
- echo "Installed FrankenPHP"
-else
- echo "FrankenPHP already installed: $(frankenphp --version 2>/dev/null | head -1)"
-fi
-
-# ── Install Composer ──────────────────────────────────────────────────────────
-
-if ! command -v composer &>/dev/null; then
- echo "Installing Composer..."
- curl -fsSL https://getcomposer.org/installer -o "${TMP_DIR}/composer-setup.php"
- php "${TMP_DIR}/composer-setup.php" --install-dir="${TMP_DIR}" --filename=composer
- ${USE_SUDO} install -m 0755 "${TMP_DIR}/composer" "${INSTALL_DIR}/composer"
- echo "Installed Composer"
-else
- echo "Composer already installed: $(composer --version 2>/dev/null | head -1)"
-fi
-
-# ── Verify ────────────────────────────────────────────────────────────────────
-
-echo ""
-"${BINARY_NAME}" --version
-echo "PHP variant installation complete."
diff --git a/pkg/build/installers/templates/setup.sh.tmpl b/pkg/build/installers/templates/setup.sh.tmpl
deleted file mode 100644
index 7fa4549..0000000
--- a/pkg/build/installers/templates/setup.sh.tmpl
+++ /dev/null
@@ -1,143 +0,0 @@
-#!/usr/bin/env bash
-# setup.sh — Full installer for {{.BinaryName}} {{.Version}}
-# Downloads the binary, installs to /usr/local/bin (or ~/.local/bin), sets up PATH and shell completions.
-#
-# Usage:
-# curl -sL {{.ScriptBaseURL}}/setup.sh | bash
-# curl -sL {{.ScriptBaseURL}}/setup.sh | bash -s -- --version {{.Version}}
-set -euo pipefail
-
-BINARY_NAME={{ shellQuote .BinaryName }}
-VERSION={{ shellQuote .Version }}
-REPO={{ shellQuote .Repo }}
-GITHUB_BASE="https://github.com/${REPO}"
-
-# ── OS / ARCH detection ──────────────────────────────────────────────────────
-
-detect_os() {
- case "$(uname -s)" in
- Linux*) echo "linux" ;;
- Darwin*) echo "darwin" ;;
- *)
- echo "Unsupported OS: $(uname -s)" >&2
- exit 1
- ;;
- esac
-}
-
-detect_arch() {
- case "$(uname -m)" in
- x86_64) echo "amd64" ;;
- aarch64|arm64) echo "arm64" ;;
- *)
- echo "Unsupported architecture: $(uname -m)" >&2
- exit 1
- ;;
- esac
-}
-
-OS="$(detect_os)"
-ARCH="$(detect_arch)"
-
-TARBALL="${BINARY_NAME}_${OS}_${ARCH}.tar.gz"
-DOWNLOAD_URL="${GITHUB_BASE}/releases/download/${VERSION}/${TARBALL}"
-
-# ── Install path ─────────────────────────────────────────────────────────────
-
-if [ -w "/usr/local/bin" ] || sudo -n true 2>/dev/null; then
- INSTALL_DIR="/usr/local/bin"
- USE_SUDO="sudo"
-else
- INSTALL_DIR="${HOME}/.local/bin"
- USE_SUDO=""
- mkdir -p "${INSTALL_DIR}"
-fi
-
-# ── Download & extract ────────────────────────────────────────────────────────
-
-TMP_DIR="$(mktemp -d)"
-trap 'rm -rf "${TMP_DIR}"' EXIT
-
-echo "Downloading ${BINARY_NAME} ${VERSION} (${OS}/${ARCH})..."
-curl -fsSL "${DOWNLOAD_URL}" -o "${TMP_DIR}/${TARBALL}"
-
-echo "Extracting..."
-tar -xzf "${TMP_DIR}/${TARBALL}" -C "${TMP_DIR}"
-
-# ── Install binary ────────────────────────────────────────────────────────────
-
-echo "Installing ${BINARY_NAME} to ${INSTALL_DIR}..."
-${USE_SUDO} install -m 0755 "${TMP_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"
-
-# ── PATH setup ────────────────────────────────────────────────────────────────
-
-if [ "${INSTALL_DIR}" = "${HOME}/.local/bin" ]; then
- PATH_LINE='export PATH="${HOME}/.local/bin:${PATH}"'
- for RC in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.profile"; do
- if [ -f "${RC}" ] && ! grep -qF '.local/bin' "${RC}" 2>/dev/null; then
- echo "" >> "${RC}"
- echo "# Added by ${BINARY_NAME} installer" >> "${RC}"
- echo "${PATH_LINE}" >> "${RC}"
- echo "Added PATH entry to ${RC}"
- fi
- done
- export PATH="${HOME}/.local/bin:${PATH}"
-fi
-
-# ── Shell completions ─────────────────────────────────────────────────────────
-
-setup_completions() {
- local shell_name="$1"
- case "${shell_name}" in
- bash)
- local comp_dir="/etc/bash_completion.d"
- if [ ! -w "${comp_dir}" ]; then
- comp_dir="${HOME}/.local/share/bash-completion/completions"
- mkdir -p "${comp_dir}"
- fi
- if "${BINARY_NAME}" completion bash > "${comp_dir}/${BINARY_NAME}" 2>/dev/null; then
- echo "Installed bash completions to ${comp_dir}/${BINARY_NAME}"
- fi
- ;;
- zsh)
- local comp_dir="${HOME}/.zsh/completions"
- mkdir -p "${comp_dir}"
- if "${BINARY_NAME}" completion zsh > "${comp_dir}/_${BINARY_NAME}" 2>/dev/null; then
- echo "Installed zsh completions to ${comp_dir}/_${BINARY_NAME}"
- # Ensure fpath is configured
- for RC in "${HOME}/.zshrc"; do
- if [ -f "${RC}" ] && ! grep -qF 'zsh/completions' "${RC}" 2>/dev/null; then
- echo "" >> "${RC}"
- echo "# ${BINARY_NAME} completions" >> "${RC}"
- echo 'fpath=("${HOME}/.zsh/completions" $fpath)' >> "${RC}"
- echo 'autoload -Uz compinit && compinit' >> "${RC}"
- fi
- done
- fi
- ;;
- fish)
- local comp_dir="${HOME}/.config/fish/completions"
- mkdir -p "${comp_dir}"
- if "${BINARY_NAME}" completion fish > "${comp_dir}/${BINARY_NAME}.fish" 2>/dev/null; then
- echo "Installed fish completions to ${comp_dir}/${BINARY_NAME}.fish"
- fi
- ;;
- esac
-}
-
-CURRENT_SHELL="$(basename "${SHELL:-bash}")"
-setup_completions "${CURRENT_SHELL}"
-
-# ── Verify ────────────────────────────────────────────────────────────────────
-
-echo ""
-echo "Verifying installation..."
-if "${BINARY_NAME}" --version; then
- echo ""
- echo "✓ ${BINARY_NAME} ${VERSION} installed successfully."
- echo " Run '${BINARY_NAME} --help' to get started."
-else
- echo "Installation may have succeeded but '${BINARY_NAME} --version' failed." >&2
- echo "Ensure ${INSTALL_DIR} is in your PATH." >&2
- exit 1
-fi
diff --git a/pkg/build/installers_example_test.go b/pkg/build/installers_example_test.go
deleted file mode 100644
index 7e4a3a7..0000000
--- a/pkg/build/installers_example_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleGenerateInstallerScript references GenerateInstallerScript on this package API surface.
-func ExampleGenerateInstallerScript() {
- _ = GenerateInstallerScript
- core.Println("GenerateInstallerScript")
- // Output: GenerateInstallerScript
-}
-
-// ExampleGenerateInstaller references GenerateInstaller on this package API surface.
-func ExampleGenerateInstaller() {
- _ = GenerateInstaller
- core.Println("GenerateInstaller")
- // Output: GenerateInstaller
-}
-
-// ExampleGenerateAllInstallerScripts references GenerateAllInstallerScripts on this package API surface.
-func ExampleGenerateAllInstallerScripts() {
- _ = GenerateAllInstallerScripts
- core.Println("GenerateAllInstallerScripts")
- // Output: GenerateAllInstallerScripts
-}
-
-// ExampleGenerateAll references GenerateAll on this package API surface.
-func ExampleGenerateAll() {
- _ = GenerateAll
- core.Println("GenerateAll")
- // Output: GenerateAll
-}
-
-// ExampleInstallerVariants references InstallerVariants on this package API surface.
-func ExampleInstallerVariants() {
- _ = InstallerVariants
- core.Println("InstallerVariants")
- // Output: InstallerVariants
-}
-
-// ExampleInstallerOutputName references InstallerOutputName on this package API surface.
-func ExampleInstallerOutputName() {
- _ = InstallerOutputName
- core.Println("InstallerOutputName")
- // Output: InstallerOutputName
-}
diff --git a/pkg/build/installers_test.go b/pkg/build/installers_test.go
deleted file mode 100644
index 25fc11b..0000000
--- a/pkg/build/installers_test.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-func TestInstallers_GenerateInstallerScript_Good(t *core.T) {
- result := GenerateInstallerScript(VariantCI, "v1.2.3", "dappcore/core")
- core.RequireTrue(t, result.OK)
- script := result.Value.(string)
- core.AssertContains(t, script, "v1.2.3")
-}
-
-func TestInstallers_GenerateInstallerScript_Bad(t *core.T) {
- result := GenerateInstallerScript(InstallerVariant("missing"), "v1.2.3", "dappcore/core")
- core.AssertFalse(t, result.OK)
- core.AssertContains(t, result.Error(), "unknown")
-}
-
-func TestInstallers_GenerateInstallerScript_Ugly(t *core.T) {
- result := GenerateInstallerScript(VariantGo, "v1.2.3", "dappcore/core.git")
- core.RequireTrue(t, result.OK)
- script := result.Value.(string)
- core.AssertContains(t, script, "core")
-}
-
-func TestInstallers_GenerateInstaller_Good(t *core.T) {
- result := GenerateInstaller(VariantFull, "v1.2.3", "dappcore/core")
- core.RequireTrue(t, result.OK)
- script := result.Value.(string)
- core.AssertContains(t, script, "v1.2.3")
-}
-
-func TestInstallers_GenerateInstaller_Bad(t *core.T) {
- result := GenerateInstaller(VariantCI, "bad version!", "dappcore/core")
- core.AssertFalse(t, result.OK)
- core.AssertContains(t, result.Error(), "version")
-}
-
-func TestInstallers_GenerateInstaller_Ugly(t *core.T) {
- result := GenerateInstaller(VariantAgentic, "v1.2.3", "")
- core.RequireTrue(t, result.OK)
- script := result.Value.(string)
- core.AssertContains(t, script, "v1.2.3")
-}
-
-func TestInstallers_GenerateAllInstallerScripts_Good(t *core.T) {
- result := GenerateAllInstallerScripts("v1.2.3", "dappcore/core")
- core.RequireTrue(t, result.OK)
- scripts := result.Value.(map[string]string)
- core.AssertContains(t, scripts, "setup.sh")
-}
-
-func TestInstallers_GenerateAllInstallerScripts_Bad(t *core.T) {
- result := GenerateAllInstallerScripts("bad version!", "dappcore/core")
- core.AssertFalse(t, result.OK)
- core.AssertContains(t, result.Error(), "version")
-}
-
-func TestInstallers_GenerateAllInstallerScripts_Ugly(t *core.T) {
- result := GenerateAllInstallerScripts("v1.2.3", "")
- core.RequireTrue(t, result.OK)
- scripts := result.Value.(map[string]string)
- core.AssertContains(t, scripts, "agent.sh")
-}
-
-func TestInstallers_GenerateAll_Good(t *core.T) {
- result := GenerateAll("v1.2.3", "dappcore/core")
- core.RequireTrue(t, result.OK)
- scripts := result.Value.(map[string]string)
- core.AssertContains(t, scripts, "go.sh")
-}
-
-func TestInstallers_GenerateAll_Bad(t *core.T) {
- result := GenerateAll("bad version!", "dappcore/core")
- core.AssertFalse(t, result.OK)
- core.AssertContains(t, result.Error(), "version")
-}
-
-func TestInstallers_GenerateAll_Ugly(t *core.T) {
- result := GenerateAll("v1.2.3", "owner/repo.git")
- core.RequireTrue(t, result.OK)
- scripts := result.Value.(map[string]string)
- core.AssertContains(t, scripts["ci.sh"], "repo")
-}
-
-func TestInstallers_InstallerVariants_Good(t *core.T) {
- variants := InstallerVariants()
- core.AssertContains(t, variants, VariantFull)
- core.AssertContains(t, variants, VariantCI)
-}
-
-func TestInstallers_InstallerVariants_Bad(t *core.T) {
- variants := InstallerVariants()
- variants[0] = InstallerVariant("mutated")
- core.AssertNotEqual(t, InstallerVariant("mutated"), InstallerVariants()[0])
-}
-
-func TestInstallers_InstallerVariants_Ugly(t *core.T) {
- variants := InstallerVariants()
- core.AssertEqual(t, VariantDev, variants[len(variants)-1])
- core.AssertLen(t, variants, 6)
-}
-
-func TestInstallers_InstallerOutputName_Good(t *core.T) {
- name := InstallerOutputName(VariantFull)
- core.AssertEqual(t, "setup.sh", name)
- core.AssertContains(t, name, ".sh")
-}
-
-func TestInstallers_InstallerOutputName_Bad(t *core.T) {
- name := InstallerOutputName(InstallerVariant("missing"))
- core.AssertEqual(t, "", name)
- core.AssertEmpty(t, name)
-}
-
-func TestInstallers_InstallerOutputName_Ugly(t *core.T) {
- name := InstallerOutputName(VariantAgentic)
- core.AssertEqual(t, "agent.sh", name)
- core.AssertContains(t, name, "agent")
-}
diff --git a/pkg/build/linuxkit_image.go b/pkg/build/linuxkit_image.go
deleted file mode 100644
index 746f3c0..0000000
--- a/pkg/build/linuxkit_image.go
+++ /dev/null
@@ -1,173 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// LinuxKitImage models an immutable LinuxKit image definition.
-//
-// image := build.LinuxKit(
-// build.WithBase("core-dev"),
-// build.WithPackages("git", "task"),
-// build.WithMount("/workspace"),
-// build.WithGPU(true),
-// )
-type LinuxKitImage struct {
- Config LinuxKitConfig
-}
-
-// LinuxKitConfig defines an immutable LinuxKit image.
-//
-// cfg := build.DefaultLinuxKitConfig()
-type LinuxKitConfig struct {
- Base string `json:"base,omitempty" yaml:"base,omitempty"`
- Packages []string `json:"packages,omitempty" yaml:"packages,omitempty"`
- Mounts []string `json:"mounts,omitempty" yaml:"mounts,omitempty"`
- GPU bool `json:"gpu,omitempty" yaml:"gpu,omitempty"`
- Formats []string `json:"formats,omitempty" yaml:"formats,omitempty"`
- Registry string `json:"registry,omitempty" yaml:"registry,omitempty"`
-}
-
-// LinuxKitOption configures an immutable LinuxKit image definition.
-type LinuxKitOption func(*LinuxKitConfig)
-
-// DefaultLinuxKitConfig returns the RFC defaults for immutable image builds.
-func DefaultLinuxKitConfig() LinuxKitConfig {
- return LinuxKitConfig{
- Base: "core-dev",
- Packages: []string{},
- Mounts: []string{"/workspace"},
- GPU: false,
- Formats: []string{"oci", "apple"},
- }
-}
-
-// LinuxKit builds an immutable LinuxKit image definition with sensible defaults.
-func LinuxKit(opts ...LinuxKitOption) *LinuxKitImage {
- cfg := DefaultLinuxKitConfig()
- for _, opt := range opts {
- if opt != nil {
- opt(&cfg)
- }
- }
- cfg = normalizeLinuxKitConfig(cfg)
- return &LinuxKitImage{Config: cfg}
-}
-
-// WithBase overrides the base image template name.
-func WithBase(base string) LinuxKitOption {
- return func(cfg *LinuxKitConfig) {
- cfg.Base = core.Trim(base)
- }
-}
-
-// WithPackages appends extra OS packages to the immutable image.
-func WithPackages(packages ...string) LinuxKitOption {
- return func(cfg *LinuxKitConfig) {
- cfg.Packages = append(cfg.Packages, packages...)
- }
-}
-
-// WithMount appends a writable mount point exposed inside the image.
-func WithMount(path string) LinuxKitOption {
- return func(cfg *LinuxKitConfig) {
- path = core.Trim(path)
- if path == "" {
- return
- }
- cfg.Mounts = append(cfg.Mounts, path)
- }
-}
-
-// WithGPU toggles GPU support for the immutable image.
-func WithGPU(enabled bool) LinuxKitOption {
- return func(cfg *LinuxKitConfig) {
- cfg.GPU = enabled
- }
-}
-
-// WithFormats overrides the requested output formats.
-func WithFormats(formats ...string) LinuxKitOption {
- return func(cfg *LinuxKitConfig) {
- cfg.Formats = normalizeLinuxKitFormats(formats)
- }
-}
-
-// WithRegistry sets the OCI registry namespace for image publication metadata.
-func WithRegistry(registry string) LinuxKitOption {
- return func(cfg *LinuxKitConfig) {
- cfg.Registry = core.Trim(registry)
- }
-}
-
-func normalizeLinuxKitValues(values []string) []string {
- if len(values) == 0 {
- return values
- }
-
- seen := make(map[string]struct{}, len(values))
- result := make([]string, 0, len(values))
- for _, value := range values {
- value = core.Trim(value)
- if value == "" {
- continue
- }
- if _, ok := seen[value]; ok {
- continue
- }
- seen[value] = struct{}{}
- result = append(result, value)
- }
-
- return result
-}
-
-func normalizeLinuxKitFormats(values []string) []string {
- if len(values) == 0 {
- return values
- }
-
- seen := make(map[string]struct{}, len(values))
- result := make([]string, 0, len(values))
- for _, value := range values {
- value = core.Lower(core.Trim(value))
- if value == "" {
- continue
- }
- if _, ok := seen[value]; ok {
- continue
- }
- seen[value] = struct{}{}
- result = append(result, value)
- }
-
- return result
-}
-
-func normalizeLinuxKitConfig(cfg LinuxKitConfig) LinuxKitConfig {
- cfg = applyLinuxKitDefaults(cfg)
-
- cfg.Base = core.Trim(cfg.Base)
- cfg.Registry = core.Trim(cfg.Registry)
- cfg.Packages = normalizeLinuxKitValues(cfg.Packages)
-
- cfg.Mounts = normalizeLinuxKitValues(cfg.Mounts)
- cfg.Formats = normalizeLinuxKitFormats(cfg.Formats)
- cfg = applyLinuxKitDefaults(cfg)
-
- return cfg
-}
-
-func applyLinuxKitDefaults(cfg LinuxKitConfig) LinuxKitConfig {
- defaults := DefaultLinuxKitConfig()
-
- if core.Trim(cfg.Base) == "" {
- cfg.Base = defaults.Base
- }
- if len(cfg.Mounts) == 0 {
- cfg.Mounts = append([]string(nil), defaults.Mounts...)
- }
- if len(cfg.Formats) == 0 {
- cfg.Formats = append([]string(nil), defaults.Formats...)
- }
-
- return cfg
-}
diff --git a/pkg/build/linuxkit_image_example_test.go b/pkg/build/linuxkit_image_example_test.go
deleted file mode 100644
index 321c147..0000000
--- a/pkg/build/linuxkit_image_example_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleDefaultLinuxKitConfig references DefaultLinuxKitConfig on this package API surface.
-func ExampleDefaultLinuxKitConfig() {
- _ = DefaultLinuxKitConfig
- core.Println("DefaultLinuxKitConfig")
- // Output: DefaultLinuxKitConfig
-}
-
-// ExampleLinuxKit references LinuxKit on this package API surface.
-func ExampleLinuxKit() {
- _ = LinuxKit
- core.Println("LinuxKit")
- // Output: LinuxKit
-}
-
-// ExampleWithBase references WithBase on this package API surface.
-func ExampleWithBase() {
- _ = WithBase
- core.Println("WithBase")
- // Output: WithBase
-}
-
-// ExampleWithPackages references WithPackages on this package API surface.
-func ExampleWithPackages() {
- _ = WithPackages
- core.Println("WithPackages")
- // Output: WithPackages
-}
-
-// ExampleWithMount references WithMount on this package API surface.
-func ExampleWithMount() {
- _ = WithMount
- core.Println("WithMount")
- // Output: WithMount
-}
-
-// ExampleWithGPU references WithGPU on this package API surface.
-func ExampleWithGPU() {
- _ = WithGPU
- core.Println("WithGPU")
- // Output: WithGPU
-}
-
-// ExampleWithFormats references WithFormats on this package API surface.
-func ExampleWithFormats() {
- _ = WithFormats
- core.Println("WithFormats")
- // Output: WithFormats
-}
-
-// ExampleWithRegistry references WithRegistry on this package API surface.
-func ExampleWithRegistry() {
- _ = WithRegistry
- core.Println("WithRegistry")
- // Output: WithRegistry
-}
diff --git a/pkg/build/linuxkit_image_test.go b/pkg/build/linuxkit_image_test.go
deleted file mode 100644
index 414ba27..0000000
--- a/pkg/build/linuxkit_image_test.go
+++ /dev/null
@@ -1,303 +0,0 @@
-package build
-
-import (
- core "dappco.re/go"
- "testing"
-)
-
-func TestBuild_DefaultLinuxKitConfig_Good(t *testing.T) {
- cfg := DefaultLinuxKitConfig()
- if !stdlibAssertEqual("core-dev", cfg.Base) {
- t.Fatalf("want %v, got %v", "core-dev", cfg.Base)
- }
- if !stdlibAssertEqual([]string{"/workspace"}, cfg.Mounts) {
- t.Fatalf("want %v, got %v", []string{"/workspace"}, cfg.Mounts)
- }
- if !stdlibAssertEqual([]string{"oci", "apple"}, cfg.Formats) {
- t.Fatalf("want %v, got %v", []string{"oci", "apple"}, cfg.Formats)
- }
- if cfg.GPU {
- t.Fatal("expected false")
- }
-
-}
-
-func TestBuild_LinuxKit_Good(t *testing.T) {
- image := LinuxKit(
- WithBase("core-ml"),
- WithPackages("git", "task"),
- WithMount("/src"),
- WithGPU(true),
- WithFormats("oci"),
- WithRegistry("ghcr.io/dappcore"),
- )
- if stdlibAssertNil(image) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(LinuxKitConfig{Base: "core-ml", Packages: []string{"git", "task"}, Mounts: []string{"/workspace", "/src"}, GPU: true, Formats: []string{"oci"}, Registry: "ghcr.io/dappcore"}, image.Config) {
- t.Fatalf("want %v, got %v", LinuxKitConfig{Base: "core-ml", Packages: []string{"git", "task"}, Mounts: []string{"/workspace", "/src"}, GPU: true, Formats: []string{"oci"}, Registry: "ghcr.io/dappcore"}, image.Config)
- }
-
-}
-
-func TestBuild_LinuxKit_NormalizesOptionValues_Good(t *testing.T) {
- image := LinuxKit(
- WithBase(" core-dev "),
- WithPackages(" git ", "git", "task"),
- WithMount("/workspace"),
- WithMount(" /src "),
- WithFormats(" OCI ", "apple", "APPLE", ""),
- WithRegistry(" ghcr.io/dappcore "),
- )
- if stdlibAssertNil(image) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(LinuxKitConfig{Base: "core-dev", Packages: []string{"git", "task"}, Mounts: []string{"/workspace", "/src"}, GPU: false, Formats: []string{"oci", "apple"}, Registry: "ghcr.io/dappcore"}, image.Config) {
- t.Fatalf("want %v, got %v", LinuxKitConfig{Base: "core-dev", Packages: []string{"git", "task"}, Mounts: []string{"/workspace", "/src"}, GPU: false, Formats: []string{"oci", "apple"}, Registry: "ghcr.io/dappcore"}, image.Config)
- }
-
-}
-
-func TestBuild_LinuxKitBaseTemplate_Good(t *testing.T) {
- images := LinuxKitBaseImages()
- if len(images) != 3 {
- t.Fatalf("want len %v, got %v", 3, len(images))
- }
-
- for _, image := range images {
- templateResult := LinuxKitBaseTemplate(image.Name)
- if !templateResult.OK {
- t.Fatalf("unexpected error: %v", templateResult.Error())
- }
- content := templateResult.Value.(string)
- if !stdlibAssertContains(content, image.Name) {
- t.Fatalf("expected %v to contain %v", content, image.Name)
- }
-
- lookedUp, ok := LookupLinuxKitBaseImage(image.Name)
- if !(ok) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(image.Name, lookedUp.Name) {
- t.Fatalf("want %v, got %v", image.Name, lookedUp.Name)
- }
-
- }
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestLinuxkitImage_DefaultLinuxKitConfig_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultLinuxKitConfig()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_DefaultLinuxKitConfig_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultLinuxKitConfig()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_DefaultLinuxKitConfig_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultLinuxKitConfig()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_LinuxKit_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = LinuxKit()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_LinuxKit_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = LinuxKit()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_LinuxKit_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = LinuxKit()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_WithBase_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBase("agent")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_WithBase_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBase("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_WithBase_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBase("agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_WithPackages_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithPackages()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_WithPackages_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithPackages()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_WithPackages_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithPackages()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_WithMount_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithMount(core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_WithMount_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithMount("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_WithMount_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithMount(core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_WithGPU_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithGPU(true)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_WithGPU_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithGPU(false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_WithGPU_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithGPU(true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_WithFormats_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithFormats()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_WithFormats_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithFormats()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_WithFormats_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithFormats()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestLinuxkitImage_WithRegistry_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithRegistry("agent")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestLinuxkitImage_WithRegistry_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithRegistry("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestLinuxkitImage_WithRegistry_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithRegistry("agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/linuxkit_templates.go b/pkg/build/linuxkit_templates.go
deleted file mode 100644
index be780c3..0000000
--- a/pkg/build/linuxkit_templates.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package build
-
-import (
- "embed"
-
- "dappco.re/go"
-)
-
-//go:embed images/*.yml
-var linuxKitBaseTemplateFS embed.FS
-
-// LinuxKitBaseImage describes a built-in immutable image template.
-type LinuxKitBaseImage struct {
- Name string
- Description string
- Version string
- DefaultPackages []string
-}
-
-var linuxKitBaseCatalog = []LinuxKitBaseImage{
- {
- Name: "core-dev",
- Description: "Go toolchain, git, task, core CLI, linters",
- Version: "2026.04.08",
- DefaultPackages: []string{"bash", "git", "go", "openssh-client", "task", "wget"},
- },
- {
- Name: "core-ml",
- Description: "Go toolchain, ML runtimes, model loaders",
- Version: "2026.04.08",
- DefaultPackages: []string{"bash", "git", "go", "python3", "py3-pip", "wget"},
- },
- {
- Name: "core-minimal",
- Description: "Go toolchain only",
- Version: "2026.04.08",
- DefaultPackages: []string{"go"},
- },
-}
-
-// LinuxKitBaseImages returns the built-in immutable image templates.
-func LinuxKitBaseImages() []LinuxKitBaseImage {
- result := make([]LinuxKitBaseImage, len(linuxKitBaseCatalog))
- for i, image := range linuxKitBaseCatalog {
- result[i] = LinuxKitBaseImage{
- Name: image.Name,
- Description: image.Description,
- Version: image.Version,
- DefaultPackages: append([]string(nil), image.DefaultPackages...),
- }
- }
- return result
-}
-
-// LookupLinuxKitBaseImage resolves a built-in immutable image template.
-func LookupLinuxKitBaseImage(name string) (LinuxKitBaseImage, bool) {
- for _, image := range linuxKitBaseCatalog {
- if image.Name == name {
- return LinuxKitBaseImage{
- Name: image.Name,
- Description: image.Description,
- Version: image.Version,
- DefaultPackages: append([]string(nil), image.DefaultPackages...),
- }, true
- }
- }
- return LinuxKitBaseImage{}, false
-}
-
-// LinuxKitBaseTemplate loads the built-in LinuxKit template for a named base image.
-func LinuxKitBaseTemplate(name string) core.Result {
- if _, ok := LookupLinuxKitBaseImage(name); !ok {
- return core.Fail(core.E("build.LinuxKitBaseTemplate", "unknown LinuxKit image base: "+name, nil))
- }
-
- content, err := linuxKitBaseTemplateFS.ReadFile("images/" + name + ".yml")
- if err != nil {
- return core.Fail(core.E("build.LinuxKitBaseTemplate", "failed to read embedded LinuxKit template", err))
- }
-
- return core.Ok(string(content))
-}
diff --git a/pkg/build/linuxkit_templates_example_test.go b/pkg/build/linuxkit_templates_example_test.go
deleted file mode 100644
index d35616d..0000000
--- a/pkg/build/linuxkit_templates_example_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleLinuxKitBaseImages references LinuxKitBaseImages on this package API surface.
-func ExampleLinuxKitBaseImages() {
- _ = LinuxKitBaseImages
- core.Println("LinuxKitBaseImages")
- // Output: LinuxKitBaseImages
-}
-
-// ExampleLookupLinuxKitBaseImage references LookupLinuxKitBaseImage on this package API surface.
-func ExampleLookupLinuxKitBaseImage() {
- _ = LookupLinuxKitBaseImage
- core.Println("LookupLinuxKitBaseImage")
- // Output: LookupLinuxKitBaseImage
-}
-
-// ExampleLinuxKitBaseTemplate references LinuxKitBaseTemplate on this package API surface.
-func ExampleLinuxKitBaseTemplate() {
- _ = LinuxKitBaseTemplate
- core.Println("LinuxKitBaseTemplate")
- // Output: LinuxKitBaseTemplate
-}
diff --git a/pkg/build/linuxkit_templates_test.go b/pkg/build/linuxkit_templates_test.go
deleted file mode 100644
index 6e0d9ab..0000000
--- a/pkg/build/linuxkit_templates_test.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-func TestLinuxkitTemplates_LinuxKitBaseImages_Good(t *core.T) {
- images := LinuxKitBaseImages()
- core.AssertLen(t, images, 3)
- core.AssertEqual(t, "core-dev", images[0].Name)
-}
-
-func TestLinuxkitTemplates_LinuxKitBaseImages_Bad(t *core.T) {
- images := LinuxKitBaseImages()
- images[0].DefaultPackages[0] = "mutated"
- again := LinuxKitBaseImages()
- core.AssertNotEqual(t, "mutated", again[0].DefaultPackages[0])
-}
-
-func TestLinuxkitTemplates_LinuxKitBaseImages_Ugly(t *core.T) {
- images := LinuxKitBaseImages()
- core.AssertEqual(t, "core-minimal", images[2].Name)
- core.AssertContains(t, images[2].DefaultPackages, "go")
-}
-
-func TestLinuxkitTemplates_LookupLinuxKitBaseImage_Good(t *core.T) {
- image, ok := LookupLinuxKitBaseImage("core-dev")
- core.AssertTrue(t, ok)
- core.AssertEqual(t, "core-dev", image.Name)
-}
-
-func TestLinuxkitTemplates_LookupLinuxKitBaseImage_Bad(t *core.T) {
- image, ok := LookupLinuxKitBaseImage("missing")
- core.AssertFalse(t, ok)
- core.AssertEqual(t, "", image.Name)
-}
-
-func TestLinuxkitTemplates_LookupLinuxKitBaseImage_Ugly(t *core.T) {
- image, ok := LookupLinuxKitBaseImage("core-minimal")
- core.AssertTrue(t, ok)
- core.AssertEqual(t, []string{"go"}, image.DefaultPackages)
-}
-
-func TestLinuxkitTemplates_LinuxKitBaseTemplate_Good(t *core.T) {
- result := LinuxKitBaseTemplate("core-dev")
- core.RequireTrue(t, result.OK)
- template := result.Value.(string)
- core.AssertContains(t, template, "CORE_IMAGE=core-dev")
-}
-
-func TestLinuxkitTemplates_LinuxKitBaseTemplate_Bad(t *core.T) {
- result := LinuxKitBaseTemplate("missing")
- core.AssertFalse(t, result.OK)
- core.AssertContains(t, result.Error(), "missing")
-}
-
-func TestLinuxkitTemplates_LinuxKitBaseTemplate_Ugly(t *core.T) {
- result := LinuxKitBaseTemplate("core-minimal")
- core.RequireTrue(t, result.OK)
- template := result.Value.(string)
- core.AssertContains(t, template, "core-minimal")
-}
diff --git a/pkg/build/options.go b/pkg/build/options.go
deleted file mode 100644
index 6854273..0000000
--- a/pkg/build/options.go
+++ /dev/null
@@ -1,224 +0,0 @@
-// Package build provides project type detection and cross-compilation for the Core build system.
-// This file handles build options computation from config + discovery.
-package build
-
-import (
- "strconv"
-
- "dappco.re/go"
-)
-
-// BuildOptions holds computed build flags from config + discovery.
-//
-// opts := build.ComputeOptions(cfg, discovery)
-// fmt.Println(opts.String()) // "-tags webkit2_41"
-type BuildOptions struct {
- // Obfuscate uses garble instead of go build for obfuscation.
- Obfuscate bool
- // Tags holds de-duplicated Go build tags.
- Tags []string
- // NSIS enables Windows NSIS installer generation (Wails only).
- NSIS bool
- // WebView2 sets the WebView2 delivery method: download|embed|browser|error.
- WebView2 string
- // LDFlags holds linker flags merged from config.
- LDFlags []string
-}
-
-// ComputeOptions merges config + discovery into build flags.
-// Handles distro-aware WebKit tag injection for Ubuntu 24.04+ Wails builds.
-// Returns safe defaults when cfg or discovery is nil.
-//
-// opts := build.ComputeOptions(cfg, result)
-// if opts.Obfuscate { /* use garble */ }
-func ComputeOptions(cfg *BuildConfig, discovery *DiscoveryResult) *BuildOptions {
- options := &BuildOptions{}
-
- if cfg != nil {
- options.Obfuscate = cfg.Build.Obfuscate
- options.NSIS = cfg.Build.NSIS
- options.WebView2 = cfg.Build.WebView2
- options.LDFlags = append(options.LDFlags, cfg.Build.LDFlags...)
- options.Tags = append(options.Tags, cfg.Build.BuildTags...)
- }
-
- // Inject webkit2_41 for Ubuntu 24.04+ Wails builds.
- if shouldInjectWebKitTag(cfg, discovery) {
- options.Tags = InjectWebKitTag(options.Tags, discovery.Distro)
- }
-
- // De-duplicate tags
- options.Tags = deduplicateTags(options.Tags)
-
- return options
-}
-
-// ApplyOptions copies computed build options onto a runtime build config.
-//
-// build.ApplyOptions(cfg, build.ComputeOptions(config, discovery))
-func ApplyOptions(cfg *Config, options *BuildOptions) {
- if cfg == nil || options == nil {
- return
- }
-
- if options.Obfuscate {
- cfg.Obfuscate = true
- }
- if options.NSIS {
- cfg.NSIS = true
- }
- if options.WebView2 != "" {
- cfg.WebView2 = options.WebView2
- }
-
- if len(options.LDFlags) > 0 {
- cfg.LDFlags = append([]string{}, options.LDFlags...)
- }
-
- if len(options.Tags) > 0 {
- cfg.BuildTags = deduplicateTags(append(cfg.BuildTags, options.Tags...))
- }
-}
-
-// InjectWebKitTag adds webkit2_41 tag for Ubuntu 24.04+ if not already present.
-// Called automatically by ComputeOptions when discovery detects Linux.
-//
-// tags := build.InjectWebKitTag(tags, "24.04") // ["webkit2_41"]
-// tags := build.InjectWebKitTag(tags, "22.04") // unchanged
-func InjectWebKitTag(tags []string, distro string) []string {
- if distro == "" {
- return tags
- }
-
- // Check if the distro version is 24.04 or newer
- if !isUbuntu2404OrNewer(distro) {
- return tags
- }
-
- // Check if tag is already present
- for _, tag := range tags {
- if tag == "webkit2_41" {
- return tags
- }
- }
-
- return append([]string{"webkit2_41"}, tags...)
-}
-
-// String returns the options as a CLI flag string.
-//
-// s := opts.String() // "-tags webkit2_41 -ldflags '-s -w'"
-func (o *BuildOptions) String() string {
- if o == nil {
- return ""
- }
-
- var parts []string
-
- if o.Obfuscate {
- parts = append(parts, "-obfuscated")
- }
-
- if len(o.Tags) > 0 {
- parts = append(parts, "-tags "+core.Join(",", o.Tags...))
- }
-
- if o.NSIS {
- parts = append(parts, "-nsis")
- }
-
- if o.WebView2 != "" {
- parts = append(parts, "-webview2 "+o.WebView2)
- }
-
- if len(o.LDFlags) > 0 {
- parts = append(parts, "-ldflags '"+core.Join(" ", o.LDFlags...)+"'")
- }
-
- return core.Join(" ", parts...)
-}
-
-func shouldInjectWebKitTag(cfg *BuildConfig, discovery *DiscoveryResult) bool {
- if discovery == nil || discovery.Distro == "" {
- return false
- }
-
- if discovery.OS != "" && core.Lower(core.Trim(discovery.OS)) != "linux" {
- return false
- }
-
- if cfg != nil && core.Lower(core.Trim(cfg.Build.Type)) == string(ProjectTypeWails) {
- return true
- }
-
- if core.Lower(core.Trim(discovery.ConfiguredType)) == string(ProjectTypeWails) {
- return true
- }
-
- if discovery.PrimaryStack == string(ProjectTypeWails) {
- return true
- }
-
- for _, projectType := range discovery.Types {
- if projectType == ProjectTypeWails {
- return true
- }
- }
-
- return false
-}
-
-// isUbuntu2404OrNewer checks if the distro version string represents Ubuntu 24.04+.
-// Compares major.minor version numerically.
-//
-// isUbuntu2404OrNewer("24.04") // true
-// isUbuntu2404OrNewer("22.04") // false
-// isUbuntu2404OrNewer("25.10") // true
-func isUbuntu2404OrNewer(distro string) bool {
- parts := core.Split(distro, ".")
- if len(parts) != 2 {
- return false
- }
-
- major, err := strconv.Atoi(parts[0])
- if err != nil {
- return false
- }
- minor, err := strconv.Atoi(parts[1])
- if err != nil {
- return false
- }
-
- // 24.04 or newer: major > 24, or major == 24 and minor >= 4
- if major > 24 {
- return true
- }
- if major == 24 && minor >= 4 {
- return true
- }
- return false
-}
-
-// deduplicateTags removes duplicate entries from a tag slice while preserving order.
-//
-// deduplicateTags([]string{"a", "b", "a"}) // ["a", "b"]
-func deduplicateTags(tags []string) []string {
- if len(tags) == 0 {
- return tags
- }
-
- seen := make(map[string]bool, len(tags))
- result := make([]string, 0, len(tags))
-
- for _, tag := range tags {
- if tag == "" {
- continue
- }
- if !seen[tag] {
- seen[tag] = true
- result = append(result, tag)
- }
- }
-
- return result
-}
diff --git a/pkg/build/options_example_test.go b/pkg/build/options_example_test.go
deleted file mode 100644
index 7008b11..0000000
--- a/pkg/build/options_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleComputeOptions references ComputeOptions on this package API surface.
-func ExampleComputeOptions() {
- _ = ComputeOptions
- core.Println("ComputeOptions")
- // Output: ComputeOptions
-}
-
-// ExampleApplyOptions references ApplyOptions on this package API surface.
-func ExampleApplyOptions() {
- _ = ApplyOptions
- core.Println("ApplyOptions")
- // Output: ApplyOptions
-}
-
-// ExampleInjectWebKitTag references InjectWebKitTag on this package API surface.
-func ExampleInjectWebKitTag() {
- _ = InjectWebKitTag
- core.Println("InjectWebKitTag")
- // Output: InjectWebKitTag
-}
-
-// ExampleBuildOptions_String references BuildOptions.String on this package API surface.
-func ExampleBuildOptions_String() {
- _ = (*BuildOptions).String
- core.Println("BuildOptions.String")
- // Output: BuildOptions.String
-}
diff --git a/pkg/build/options_test.go b/pkg/build/options_test.go
deleted file mode 100644
index 0924fc2..0000000
--- a/pkg/build/options_test.go
+++ /dev/null
@@ -1,652 +0,0 @@
-package build
-
-import (
- core "dappco.re/go"
- "testing"
-)
-
-// --- ComputeOptions ---
-
-func TestOptions_ComputeOptions_Good(t *testing.T) {
- t.Run("normal config produces correct options", func(t *testing.T) {
- cfg := &BuildConfig{
- Build: Build{
- Obfuscate: true,
- NSIS: true,
- WebView2: "embed",
- BuildTags: []string{"integration"},
- LDFlags: []string{"-s", "-w"},
- },
- }
- discovery := &DiscoveryResult{
- Types: []ProjectType{ProjectTypeWails},
- PrimaryStack: "wails",
- Distro: "24.04",
- }
-
- opts := ComputeOptions(cfg, discovery)
- if stdlibAssertNil(opts) {
- t.Fatal("expected non-nil")
- }
- if !(opts.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(opts.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("embed", opts.WebView2) {
- t.Fatalf("want %v, got %v", "embed", opts.WebView2)
- }
- if !stdlibAssertEqual([]string{
-
- // webkit2_41 injected for 24.04
- "-s", "-w"}, opts.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w"}, opts.LDFlags)
- }
- if !stdlibAssertEqual([]string{"webkit2_41", "integration"}, opts.Tags) {
- t.Fatalf("want %v, got %v", []string{"webkit2_41", "integration"}, opts.Tags)
- }
- if !stdlibAssertContains(opts.Tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", opts.Tags, "webkit2_41")
- }
-
- })
-
- t.Run("discovery with non-Ubuntu distro leaves tags empty", func(t *testing.T) {
- cfg := &BuildConfig{
- Build: Build{
- LDFlags: []string{"-s"},
- },
- }
- discovery := &DiscoveryResult{
- Types: []ProjectType{ProjectTypeWails},
- PrimaryStack: "wails",
- Distro: "22.04",
- }
-
- opts := ComputeOptions(cfg, discovery)
- if stdlibAssertNil(opts) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEmpty(opts.Tags) {
- t.Fatalf("expected empty, got %v", opts.Tags)
- }
-
- })
-
- t.Run("discovery with 25.10 distro injects webkit tag", func(t *testing.T) {
- opts := ComputeOptions(&BuildConfig{}, &DiscoveryResult{
- Types: []ProjectType{ProjectTypeWails},
- PrimaryStack: "wails",
- Distro: "25.10",
- })
- if !stdlibAssertContains(opts.Tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", opts.Tags, "webkit2_41")
- }
-
- })
-
- t.Run("non-Wails stacks do not inject webkit tag", func(t *testing.T) {
- opts := ComputeOptions(&BuildConfig{}, &DiscoveryResult{
- Types: []ProjectType{ProjectTypeGo},
- PrimaryStack: "go",
- Distro: "24.04",
- })
- if stdlibAssertContains(opts.Tags, "webkit2_41") {
- t.Fatalf("expected %v not to contain %v", opts.Tags, "webkit2_41")
- }
-
- })
-
- t.Run("configured wails type injects webkit tag even when discovery markers differ", func(t *testing.T) {
- opts := ComputeOptions(&BuildConfig{
- Build: Build{
- Type: "WaIlS",
- },
- }, &DiscoveryResult{
- Types: []ProjectType{ProjectTypeGo},
- PrimaryStack: "go",
- Distro: "24.04",
- })
- if !stdlibAssertContains(opts.Tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", opts.Tags, "webkit2_41")
- }
-
- })
-
- t.Run("configured discovery type injects webkit tag even without build config type", func(t *testing.T) {
- opts := ComputeOptions(&BuildConfig{}, &DiscoveryResult{
- ConfiguredType: string(ProjectTypeWails),
- Distro: "24.04",
- })
- if !stdlibAssertContains(opts.Tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", opts.Tags, "webkit2_41")
- }
-
- })
-
- t.Run("discovery types alone can trigger webkit injection", func(t *testing.T) {
- opts := ComputeOptions(&BuildConfig{}, &DiscoveryResult{
- Types: []ProjectType{ProjectTypeWails, ProjectTypeGo},
- PrimaryStack: "go",
- Distro: "24.04",
- })
- if !stdlibAssertContains(opts.Tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", opts.Tags, "webkit2_41")
- }
-
- })
-}
-
-func TestOptions_ComputeOptions_Bad(t *testing.T) {
- t.Run("nil config returns safe defaults", func(t *testing.T) {
- discovery := &DiscoveryResult{
- Types: []ProjectType{ProjectTypeWails},
- PrimaryStack: "wails",
- Distro: "24.04",
- }
-
- opts := ComputeOptions(nil, discovery)
- if stdlibAssertNil(opts) {
- t.Fatal("expected non-nil")
- }
- if opts.Obfuscate {
- t.Fatal("expected false")
- }
- if opts.NSIS {
- t.Fatal("expected false")
- }
- if !stdlibAssertEmpty(opts.
-
- // webkit2_41 still injected for Wails discovery
- WebView2) {
- t.Fatalf("expected empty, got %v", opts.WebView2)
- }
- if !stdlibAssertEmpty(opts.LDFlags) {
- t.Fatalf("expected empty, got %v", opts.LDFlags)
- }
- if !stdlibAssertContains(opts.Tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", opts.Tags, "webkit2_41")
- }
-
- })
-
- t.Run("nil discovery skips webkit injection", func(t *testing.T) {
- cfg := &BuildConfig{
- Build: Build{
- Obfuscate: true,
- BuildTags: []string{"existing"},
- },
- }
-
- opts := ComputeOptions(cfg, nil)
- if stdlibAssertNil(opts) {
- t.Fatal("expected non-nil")
- }
- if !(opts.Obfuscate) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual([]string{"existing"}, opts.Tags) {
- t.Fatalf("want %v, got %v", []string{"existing"}, opts.Tags)
- }
-
- })
-
- t.Run("both nil returns empty options", func(t *testing.T) {
- opts := ComputeOptions(nil, nil)
- if stdlibAssertNil(opts) {
- t.Fatal("expected non-nil")
- }
- if opts.Obfuscate {
- t.Fatal("expected false")
- }
- if opts.NSIS {
- t.Fatal("expected false")
- }
- if !stdlibAssertEmpty(opts.Tags) {
- t.Fatalf("expected empty, got %v", opts.Tags)
- }
- if !stdlibAssertEmpty(opts.LDFlags) {
- t.Fatalf("expected empty, got %v",
-
- // Seed webkit2_41 before discovery also injects it
- opts.LDFlags)
- }
-
- })
-}
-
-func TestOptions_ComputeOptions_Ugly(t *testing.T) {
- t.Run("duplicate tags from deduplication", func(t *testing.T) {
-
- cfg := &BuildConfig{
- Build: Build{
- BuildTags: []string{"integration", "integration", "ui"},
- },
- }
- discovery := &DiscoveryResult{Distro: "24.04"}
- discovery.Types = []ProjectType{ProjectTypeWails}
- discovery.PrimaryStack = "wails"
-
- opts := ComputeOptions(cfg, discovery)
-
- // Even though InjectWebKitTag is called once, deduplication must hold
- count := 0
- for _, tag := range opts.Tags {
- if tag == "webkit2_41" {
- count++
- }
- }
- if !stdlibAssertEqual(1, count) {
- t.Fatal("webkit2_41 must appear exactly once")
- }
- if !stdlibAssertEqual([]string{"webkit2_41", "integration", "ui"}, opts.Tags) {
- t.Fatalf("want %v, got %v", []string{"webkit2_41", "integration", "ui"}, opts.Tags)
- }
-
- })
-
- t.Run("empty distro in discovery produces no webkit tag", func(t *testing.T) {
- opts := ComputeOptions(&BuildConfig{}, &DiscoveryResult{
- Types: []ProjectType{ProjectTypeWails},
- PrimaryStack: "wails",
- Distro: "",
- })
- if !stdlibAssertEmpty(opts.Tags) {
- t.Fatalf("expected empty, got %v", opts.Tags)
- }
-
- })
-
- t.Run("all flags set simultaneously do not conflict", func(t *testing.T) {
- cfg := &BuildConfig{
- Build: Build{
- Obfuscate: true,
- NSIS: true,
- WebView2: "download",
- LDFlags: []string{"-s", "-w", "-X main.version=v1.0.0"},
- },
- }
- discovery := &DiscoveryResult{
- Types: []ProjectType{ProjectTypeWails},
- PrimaryStack: "wails",
- Distro: "24.04",
- }
-
- opts := ComputeOptions(cfg, discovery)
- if !(opts.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(opts.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("download", opts.WebView2) {
- t.Fatalf("want %v, got %v", "download", opts.WebView2)
- }
- if !stdlibAssertEqual([]string{"-s", "-w", "-X main.version=v1.0.0"},
-
- // --- InjectWebKitTag ---
- opts.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w", "-X main.version=v1.0.0"}, opts.LDFlags)
- }
- if !stdlibAssertContains(opts.
-
- // InjectWebKitTag(tags, "24.04") → ["webkit2_41"]
- Tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", opts.Tags, "webkit2_41")
- }
-
- })
-}
-
-func TestOptions_InjectWebKitTag_Good(t *testing.T) {
- t.Run("24.04 adds webkit2_41", func(t *testing.T) {
-
- tags := InjectWebKitTag(nil, "24.04")
- if !stdlibAssertEqual([]string{"webkit2_41"}, tags) {
- t.Fatalf("want %v, got %v", []string{"webkit2_41"}, tags)
- }
-
- })
-
- t.Run("24.10 adds webkit2_41", func(t *testing.T) {
- tags := InjectWebKitTag([]string{}, "24.10")
- if !stdlibAssertContains(tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", tags, "webkit2_41")
- }
-
- })
-
- t.Run("25.04 adds webkit2_41", func(t *testing.T) {
- tags := InjectWebKitTag(nil, "25.04")
- if !stdlibAssertContains(tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", tags, "webkit2_41")
- }
-
- })
-
- t.Run("existing tags are preserved before webkit2_41", func(t *testing.T) {
- existing := []string{"foo", "bar"}
- tags := InjectWebKitTag(existing, "24.04")
- if !stdlibAssertContains(tags, "webkit2_41") {
- t.Fatalf("expected %v to contain %v", tags, "webkit2_41")
- }
- if !stdlibAssertContains(tags, "foo") {
- t.Fatalf("expected %v to contain %v", tags, "foo")
- }
- if !stdlibAssertContains(tags, "bar") {
- t.Fatalf(
-
- // InjectWebKitTag(nil, "22.04") → unchanged (nil)
- "expected %v to contain %v", tags, "bar")
- }
-
- })
-}
-
-func TestOptions_InjectWebKitTag_Bad(t *testing.T) {
- t.Run("22.04 does not add tag", func(t *testing.T) {
-
- tags := InjectWebKitTag(nil, "22.04")
- if !stdlibAssertEmpty(tags) {
- t.Fatalf("expected empty, got %v", tags)
- }
-
- })
-
- t.Run("23.10 does not add tag", func(t *testing.T) {
- tags := InjectWebKitTag([]string{"existing"}, "23.10")
- if stdlibAssertContains(tags, "webkit2_41") {
- t.Fatalf("expected %v not to contain %v", tags, "webkit2_41")
- }
-
- })
-}
-
-func TestOptions_InjectWebKitTag_Ugly(t *testing.T) {
- t.Run("tag already present — not duplicated", func(t *testing.T) {
- // InjectWebKitTag(["webkit2_41"], "24.04") → ["webkit2_41"] (unchanged)
- tags := InjectWebKitTag([]string{"webkit2_41"}, "24.04")
- count := 0
- for _, tag := range tags {
- if tag == "webkit2_41" {
- count++
- }
- }
- if !stdlibAssertEqual(1, count) {
- t.Fatalf("want %v, got %v", 1, count)
- }
-
- })
-
- t.Run("empty distro returns tags unchanged", func(t *testing.T) {
- input := []string{"foo"}
- tags := InjectWebKitTag(input, "")
- if !stdlibAssertEqual(input, tags) {
- t.Fatalf("want %v, got %v", input, tags)
- }
-
- })
-
- t.Run("malformed version — no dot — returns tags unchanged", func(t *testing.T) {
- // isUbuntu2404OrNewer("2404") → false (no dot)
- tags := InjectWebKitTag(nil, "2404")
- if !stdlibAssertEmpty(tags) {
- t.Fatalf("expected empty, got %v", tags)
- }
-
- })
-
- t.Run("malformed version — non-numeric major — returns unchanged", func(t *testing.T) {
- tags := InjectWebKitTag(nil, "ubuntu.04")
- if !stdlibAssertEmpty(tags) {
- t.Fatalf("expected empty, got %v", tags)
- }
-
- })
-
- t.Run("malformed version — non-numeric minor — returns unchanged", func(t *testing.T) {
- tags := InjectWebKitTag(nil, "24.lts")
- if !stdlibAssertEmpty(tags) {
- t.Fatalf(
-
- // --- ApplyOptions ---
- "expected empty, got %v", tags)
- }
-
- })
-}
-
-func TestOptions_ApplyOptions_Good(t *testing.T) {
- t.Run("copies computed options onto runtime config", func(t *testing.T) {
- cfg := &Config{
- BuildTags: []string{"existing"},
- LDFlags: []string{"-s"},
- }
- options := &BuildOptions{
- Obfuscate: true,
- Tags: []string{"webkit2_41", "integration"},
- NSIS: true,
- WebView2: "embed",
- LDFlags: []string{"-trimpath", "-w"},
- }
-
- ApplyOptions(cfg, options)
- if !(cfg.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(cfg.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("embed", cfg.WebView2) {
- t.Fatalf("want %v, got %v", "embed", cfg.WebView2)
- }
- if !stdlibAssertEqual([]string{"-trimpath", "-w"}, cfg.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-trimpath", "-w"}, cfg.LDFlags)
- }
- if !stdlibAssertEqual([]string{"existing", "webkit2_41", "integration"}, cfg.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"existing", "webkit2_41", "integration"}, cfg.BuildTags)
- }
-
- })
-}
-
-func TestOptions_ApplyOptions_Bad(t *testing.T) {
- t.Run("nil config is ignored", func(t *testing.T) {
- func() {
- defer func() {
- if recovered := recover(); recovered != nil {
- t.Fatalf("expected no panic, got %v", recovered)
- }
- }()
- (func() {
- ApplyOptions(nil, &BuildOptions{Obfuscate: true})
- })()
- }()
-
- })
-
- t.Run("nil options are ignored", func(t *testing.T) {
- cfg := &Config{BuildTags: []string{"existing"}}
- func() {
- defer func() {
- if recovered := recover(); recovered != nil {
- t.Fatalf("expected no panic, got %v", recovered)
- }
- }()
- (func() {
- ApplyOptions(cfg, nil)
- })()
- }()
- if !stdlibAssertEqual([]string{"existing"}, cfg.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"existing"}, cfg.BuildTags)
- }
-
- })
-}
-
-func TestOptions_ApplyOptions_Ugly(t *testing.T) {
- t.Run("empty options leaves config unchanged", func(t *testing.T) {
- cfg := &Config{
- BuildTags: []string{"existing"},
- LDFlags: []string{"-s"},
- Obfuscate: true,
- NSIS: true,
- WebView2: "browser",
- }
-
- ApplyOptions(cfg, &BuildOptions{})
- if !(cfg.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(cfg.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("browser", cfg.WebView2) {
- t.Fatalf("want %v, got %v", "browser", cfg.WebView2)
- }
- if !stdlibAssertEqual([]string{"-s"},
-
- // --- String ---
- cfg.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s"}, cfg.LDFlags)
- }
- if !stdlibAssertEqual([]string{"existing"}, cfg.BuildTags) {
- t.Fatalf(
-
- // opts.String() // "-tags webkit2_41"
- "want %v, got %v", []string{"existing"}, cfg.BuildTags)
- }
-
- })
-}
-
-func TestOptions_String_Good(t *testing.T) {
- t.Run("tags only produces correct string", func(t *testing.T) {
-
- opts := &BuildOptions{Tags: []string{"webkit2_41"}}
- if !stdlibAssertEqual("-tags webkit2_41", opts.String()) {
- t.Fatalf("want %v, got %v", "-tags webkit2_41", opts.String())
- }
-
- })
-
- t.Run("ldflags only produces correct string", func(t *testing.T) {
- opts := &BuildOptions{LDFlags: []string{"-s", "-w"}}
- if !stdlibAssertEqual("-ldflags '-s -w'", opts.String()) {
- t.Fatalf("want %v, got %v", "-ldflags '-s -w'", opts.String())
- }
-
- })
-
- t.Run("tags and ldflags are space-separated", func(t *testing.T) {
- opts := &BuildOptions{
- Tags: []string{"webkit2_41"},
- LDFlags: []string{"-s", "-w"},
- }
- s := opts.String()
- if !stdlibAssertContains(s, "-tags webkit2_41") {
- t.Fatalf("expected %v to contain %v", s, "-tags webkit2_41")
- }
- if !stdlibAssertContains(s, "-ldflags '-s -w'") {
- t.Fatalf("expected %v to contain %v", s, "-ldflags '-s -w'")
- }
-
- })
-
- t.Run("empty options returns empty string", func(t *testing.T) {
- opts := &BuildOptions{}
- if !stdlibAssertEqual("", opts.String()) {
- t.Fatalf("want %v, got %v", "", opts.String())
- }
-
- })
-}
-
-func TestOptions_String_Bad(t *testing.T) {
- t.Run("nil receiver returns empty string", func(t *testing.T) {
- // var opts *BuildOptions; opts.String() → ""
- var opts *BuildOptions
- if !stdlibAssertEqual("", opts.String()) {
- t.Fatalf("want %v, got %v", "", opts.String())
- }
-
- })
-}
-
-func TestOptions_String_Ugly(t *testing.T) {
- t.Run("all fields set simultaneously", func(t *testing.T) {
- // s := opts.String() // "-obfuscated -tags webkit2_41 -nsis -webview2 embed -ldflags '-s -w'"
- opts := &BuildOptions{
- Obfuscate: true,
- Tags: []string{"webkit2_41"},
- NSIS: true,
- WebView2: "embed",
- LDFlags: []string{"-s", "-w"},
- }
- s := opts.String()
- if !stdlibAssertContains(s, "-obfuscated") {
- t.Fatalf("expected %v to contain %v", s, "-obfuscated")
- }
- if !stdlibAssertContains(s, "-tags webkit2_41") {
- t.Fatalf("expected %v to contain %v", s, "-tags webkit2_41")
- }
- if !stdlibAssertContains(s, "-nsis") {
- t.Fatalf("expected %v to contain %v", s, "-nsis")
- }
- if !stdlibAssertContains(s, "-webview2 embed") {
- t.Fatalf("expected %v to contain %v", s, "-webview2 embed")
- }
- if !stdlibAssertContains(s, "-ldflags '-s -w'") {
- t.Fatalf("expected %v to contain %v", s, "-ldflags '-s -w'")
- }
-
- })
-
- t.Run("multiple tags joined with comma", func(t *testing.T) {
- opts := &BuildOptions{Tags: []string{"webkit2_41", "integration"}}
- if !stdlibAssertEqual("-tags webkit2_41,integration", opts.String()) {
- t.Fatalf("want %v, got %v", "-tags webkit2_41,integration", opts.String())
- }
-
- })
-
- t.Run("webview2 without other flags is isolated", func(t *testing.T) {
- opts := &BuildOptions{WebView2: "browser"}
- if !stdlibAssertEqual("-webview2 browser", opts.String()) {
- t.Fatalf("want %v, got %v", "-webview2 browser", opts.String())
- }
-
- })
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestOptions_BuildOptions_String_Good(t *core.T) {
- subject := &BuildOptions{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.String()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestOptions_BuildOptions_String_Bad(t *core.T) {
- subject := &BuildOptions{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.String()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestOptions_BuildOptions_String_Ugly(t *core.T) {
- subject := &BuildOptions{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.String()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/pipeline.go b/pkg/build/pipeline.go
deleted file mode 100644
index 89661bf..0000000
--- a/pkg/build/pipeline.go
+++ /dev/null
@@ -1,440 +0,0 @@
-package build
-
-import (
- "context"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// BuilderResolver resolves a project type into a concrete builder.
-//
-// resolver := func(projectType build.ProjectType) core.Result { return builders.ResolveBuilder(projectType) }
-type BuilderResolver func(ProjectType) core.Result
-
-// VersionResolver determines the build version for a project directory.
-//
-// resolver := func(ctx context.Context, dir string) core.Result { return release.DetermineVersionWithContext(ctx, dir) }
-type VersionResolver func(context.Context, string) core.Result
-
-// Pipeline coordinates the action-style gateway phases for a build request:
-// discovery, option computation, setup planning, builder resolution, and build.
-//
-// pipeline := &build.Pipeline{FS: storage.Local, ResolveBuilder: resolver}
-type Pipeline struct {
- FS storage.Medium
- ResolveBuilder BuilderResolver
- ResolveVersion VersionResolver
-}
-
-// PipelineRequest captures the inputs required to plan or run a build.
-type PipelineRequest struct {
- ProjectDir string
- ConfigPath string
- BuildConfig *BuildConfig
- BuildType string
- BuildTags []string
- Obfuscate bool
- ObfuscateSet bool
- NSIS bool
- NSISSet bool
- WebView2 string
- WebView2Set bool
- DenoBuild string
- DenoBuildSet bool
- NpmBuild string
- NpmBuildSet bool
- BuildCache bool
- BuildCacheSet bool
- OutputDir string
- BuildName string
- Targets []Target
- Push bool
- ImageName string
- Version string
-}
-
-// PipelinePlan is the fully resolved gateway state before the builder runs.
-type PipelinePlan struct {
- ProjectDir string
- ProjectTypes []ProjectType
- BuildConfig *BuildConfig
- ProjectType ProjectType
- Builders []Builder
- Builder Builder
- Discovery *DiscoveryResult
- Options *BuildOptions
- SetupPlan *SetupPlan
- Targets []Target
- OutputDir string
- BuildName string
- Version string
- RuntimeConfig *Config
-}
-
-// PipelineResult contains the executed plan and the produced artifacts.
-type PipelineResult struct {
- Plan *PipelinePlan
- Artifacts []Artifact
-}
-
-// Plan resolves the action-style gateway phases without executing the builder.
-//
-// result := pipeline.Plan(ctx, build.PipelineRequest{ProjectDir: "."})
-func (p *Pipeline) Plan(ctx context.Context, req PipelineRequest) core.Result {
- if ctx == nil {
- ctx = context.Background()
- }
-
- filesystem := p.FS
- if filesystem == nil {
- filesystem = storage.Local
- }
-
- projectDir := req.ProjectDir
- if projectDir == "" {
- wd := ax.Getwd()
- if !wd.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "failed to get working directory", core.NewError(wd.Error())))
- }
- projectDir = wd.Value.(string)
- }
- projectDir = ax.Clean(projectDir)
-
- buildConfigResult := p.loadBuildConfig(filesystem, projectDir, req)
- if !buildConfigResult.OK {
- return buildConfigResult
- }
- buildConfig := buildConfigResult.Value.(*BuildConfig)
- buildConfig = CloneBuildConfig(buildConfig)
- applyPipelineBuildOverrides(buildConfig, req)
-
- cacheSetup := SetupBuildCache(filesystem, projectDir, buildConfig)
- if !cacheSetup.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "failed to set up build cache", core.NewError(cacheSetup.Error())))
- }
-
- discoveryResult := DiscoverFull(filesystem, projectDir)
- if !discoveryResult.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "failed to inspect project", core.NewError(discoveryResult.Error())))
- }
- discovery := discoveryResult.Value.(*DiscoveryResult)
-
- options := ComputeOptions(buildConfig, discovery)
- setupPlanResult := ComputeSetupPlan(filesystem, projectDir, buildConfig, discovery)
- if !setupPlanResult.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "failed to compute setup plan", core.NewError(setupPlanResult.Error())))
- }
- setupPlan := setupPlanResult.Value.(*SetupPlan)
-
- projectTypesResult := resolvePipelineProjectTypes(filesystem, projectDir, req.BuildType, buildConfig)
- if !projectTypesResult.OK {
- return projectTypesResult
- }
- projectTypes := projectTypesResult.Value.([]ProjectType)
-
- builders := make([]Builder, 0, len(projectTypes))
- for _, projectType := range projectTypes {
- builderResult := p.resolveBuilder(projectType)
- if !builderResult.OK {
- return builderResult
- }
- builder := builderResult.Value.(Builder)
- builders = append(builders, builder)
- }
-
- targets := req.Targets
- if len(targets) == 0 {
- if shouldUseLocalTargetByDefault(filesystem, projectDir, req) {
- targets = []Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
- } else if len(buildConfig.Targets) > 0 {
- targets = buildConfig.ToTargets()
- } else {
- targets = []Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
- }
- }
-
- outputDir := req.OutputDir
- if outputDir == "" {
- outputDir = "dist"
- }
- if !ax.IsAbs(outputDir) {
- outputDir = ax.Join(projectDir, outputDir)
- }
- outputDir = ax.Clean(outputDir)
-
- buildName := ResolveBuildName(projectDir, buildConfig, req.BuildName)
-
- version := req.Version
- if version == "" && p.ResolveVersion != nil {
- versionResult := p.ResolveVersion(ctx, projectDir)
- if !versionResult.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "failed to determine build version", core.NewError(versionResult.Error())))
- }
- version = versionResult.Value.(string)
- }
- if version != "" {
- valid := ValidateVersionString(version)
- if !valid.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "invalid build version override", core.NewError(valid.Error())))
- }
- }
-
- runtimeCfg := RuntimeConfigFromBuildConfig(filesystem, projectDir, outputDir, buildName, buildConfig, req.Push, req.ImageName, version)
- ApplyOptions(runtimeCfg, options)
-
- return core.Ok(&PipelinePlan{
- ProjectDir: projectDir,
- ProjectTypes: append([]ProjectType(nil), projectTypes...),
- BuildConfig: buildConfig,
- ProjectType: projectTypes[0],
- Builders: builders,
- Builder: builders[0],
- Discovery: discovery,
- Options: options,
- SetupPlan: setupPlan,
- Targets: append([]Target(nil), targets...),
- OutputDir: outputDir,
- BuildName: buildName,
- Version: version,
- RuntimeConfig: runtimeCfg,
- })
-}
-
-// Run executes the builder for a precomputed plan.
-//
-// result := pipeline.Run(ctx, plan)
-func (p *Pipeline) Run(ctx context.Context, plan *PipelinePlan) core.Result {
- if ctx == nil {
- ctx = context.Background()
- }
- if plan == nil {
- return core.Fail(core.E("build.Pipeline.Run", "pipeline plan is nil", nil))
- }
- if plan.RuntimeConfig == nil {
- return core.Fail(core.E("build.Pipeline.Run", "pipeline plan is missing runtime config", nil))
- }
-
- builders := append([]Builder(nil), plan.Builders...)
- projectTypes := append([]ProjectType(nil), plan.ProjectTypes...)
- if len(builders) == 0 {
- if plan.Builder == nil {
- return core.Fail(core.E("build.Pipeline.Run", "pipeline plan is missing a builder", nil))
- }
- builders = []Builder{plan.Builder}
- if len(projectTypes) == 0 && plan.ProjectType != "" {
- projectTypes = []ProjectType{plan.ProjectType}
- }
- }
- if len(projectTypes) == 0 {
- return core.Fail(core.E("build.Pipeline.Run", "pipeline plan is missing project types", nil))
- }
-
- artifacts := make([]Artifact, 0, len(builders))
- multiType := len(builders) > 1
- for i, builder := range builders {
- if builder == nil {
- return core.Fail(core.E("build.Pipeline.Run", "pipeline plan contains a nil builder", nil))
- }
-
- runtimeCfg := plan.RuntimeConfig
- if multiType {
- runtimeCfg = cloneRuntimeConfig(plan.RuntimeConfig)
- runtimeCfg.OutputDir = multiTypeOutputDir(plan.OutputDir, projectTypes, i)
- }
-
- builtArtifacts := builder.Build(ctx, runtimeCfg, plan.Targets)
- if !builtArtifacts.OK {
- return builtArtifacts
- }
- artifacts = append(artifacts, builtArtifacts.Value.([]Artifact)...)
- }
-
- return core.Ok(&PipelineResult{
- Plan: plan,
- Artifacts: artifacts,
- })
-}
-
-// ResolveBuildName resolves the output name from an explicit override, config,
-// or the project directory name.
-//
-// name := build.ResolveBuildName("/tmp/project", cfg, "")
-func ResolveBuildName(projectDir string, cfg *BuildConfig, override string) string {
- if override != "" {
- return override
- }
- if cfg != nil {
- if cfg.Project.Binary != "" {
- return cfg.Project.Binary
- }
- if cfg.Project.Name != "" {
- return cfg.Project.Name
- }
- }
- return ax.Base(projectDir)
-}
-
-func (p *Pipeline) loadBuildConfig(filesystem storage.Medium, projectDir string, req PipelineRequest) core.Result {
- if req.BuildConfig != nil {
- return core.Ok(req.BuildConfig)
- }
-
- if req.ConfigPath == "" {
- cfg := LoadConfig(filesystem, projectDir)
- if !cfg.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "failed to load config", core.NewError(cfg.Error())))
- }
- return cfg
- }
-
- configPath := req.ConfigPath
- if !ax.IsAbs(configPath) {
- configPath = ax.Join(projectDir, configPath)
- }
- if !filesystem.Exists(configPath) {
- return core.Fail(core.E("build.Pipeline.Plan", "build config not found: "+configPath, nil))
- }
-
- cfg := LoadConfigAtPath(filesystem, configPath)
- if !cfg.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "failed to load config", core.NewError(cfg.Error())))
- }
- return cfg
-}
-
-func (p *Pipeline) resolveBuilder(projectType ProjectType) core.Result {
- if p.ResolveBuilder == nil {
- return core.Fail(core.E("build.Pipeline.Plan", "builder resolver is required", nil))
- }
-
- builderResult := p.ResolveBuilder(projectType)
- if !builderResult.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "failed to resolve builder for "+string(projectType), core.NewError(builderResult.Error())))
- }
- builder := builderResult.Value.(Builder)
- if builder == nil {
- return core.Fail(core.E("build.Pipeline.Plan", "builder resolver returned nil for "+string(projectType), nil))
- }
-
- return core.Ok(builder)
-}
-
-func resolvePipelineProjectTypes(filesystem storage.Medium, projectDir, buildType string, cfg *BuildConfig) core.Result {
- if value := normalisePipelineBuildType(buildType); value != "" {
- return core.Ok([]ProjectType{ProjectType(value)})
- }
- if cfg != nil {
- if value := normalisePipelineBuildType(cfg.Build.Type); value != "" {
- return core.Ok([]ProjectType{ProjectType(value)})
- }
- }
-
- projectTypesResult := Discover(filesystem, projectDir)
- if !projectTypesResult.OK {
- return core.Fail(core.E("build.Pipeline.Plan", "failed to detect project type", core.NewError(projectTypesResult.Error())))
- }
- projectTypes := projectTypesResult.Value.([]ProjectType)
- if len(projectTypes) == 0 {
- return core.Fail(core.E("build.Pipeline.Plan", "no buildable project type found in "+projectDir, nil))
- }
-
- return projectTypesResult
-}
-
-func shouldUseLocalTargetByDefault(filesystem storage.Medium, projectDir string, req PipelineRequest) bool {
- if req.BuildConfig != nil || req.ConfigPath != "" {
- return false
- }
-
- return !ConfigExists(filesystem, projectDir)
-}
-
-func applyPipelineBuildOverrides(cfg *BuildConfig, req PipelineRequest) {
- if cfg == nil {
- return
- }
-
- if cfg.Build.Type != "" {
- cfg.Build.Type = normalisePipelineBuildType(cfg.Build.Type)
- }
- if buildType := normalisePipelineBuildType(req.BuildType); buildType != "" {
- cfg.Build.Type = buildType
- }
- if len(req.BuildTags) > 0 {
- cfg.Build.BuildTags = deduplicateTags(append([]string(nil), req.BuildTags...))
- }
- if req.ObfuscateSet {
- cfg.Build.Obfuscate = req.Obfuscate
- }
- if req.NSISSet {
- cfg.Build.NSIS = req.NSIS
- }
- if req.WebView2Set {
- cfg.Build.WebView2 = req.WebView2
- }
- if req.DenoBuildSet {
- cfg.Build.DenoBuild = req.DenoBuild
- }
- if req.NpmBuildSet {
- cfg.Build.NpmBuild = req.NpmBuild
- }
- if req.BuildCacheSet {
- if req.BuildCache {
- enableDefaultPipelineBuildCache(&cfg.Build.Cache)
- } else {
- cfg.Build.Cache.Enabled = false
- }
- }
-}
-
-func cloneRuntimeConfig(cfg *Config) *Config {
- if cfg == nil {
- return nil
- }
-
- clone := *cfg
- clone.LDFlags = append([]string(nil), cfg.LDFlags...)
- clone.Flags = append([]string(nil), cfg.Flags...)
- clone.BuildTags = append([]string(nil), cfg.BuildTags...)
- clone.Env = append([]string(nil), cfg.Env...)
- clone.Cache = cloneCacheConfig(cfg.Cache)
- clone.Tags = append([]string(nil), cfg.Tags...)
- clone.BuildArgs = CloneStringMap(cfg.BuildArgs)
- clone.Formats = append([]string(nil), cfg.Formats...)
- clone.LinuxKit = cloneLinuxKitConfig(cfg.LinuxKit)
- return &clone
-}
-
-func multiTypeOutputDir(root string, projectTypes []ProjectType, index int) string {
- if root == "" || index < 0 || index >= len(projectTypes) || projectTypes[index] == "" {
- return root
- }
- return ax.Join(root, string(projectTypes[index]))
-}
-
-func enableDefaultPipelineBuildCache(cfg *CacheConfig) {
- if cfg == nil {
- return
- }
-
- cfg.Enabled = true
- if cfg.Dir == "" && cfg.Directory == "" {
- cfg.Dir = ax.Join(ConfigDir, "cache")
- }
- if cfg.Dir == "" {
- cfg.Dir = cfg.Directory
- }
- if cfg.Directory == "" {
- cfg.Directory = cfg.Dir
- }
- if len(cfg.Paths) == 0 {
- cfg.Paths = DefaultBuildCachePaths("")
- }
-}
-
-func normalisePipelineBuildType(value string) string {
- return core.Lower(core.Trim(value))
-}
diff --git a/pkg/build/pipeline_example_test.go b/pkg/build/pipeline_example_test.go
deleted file mode 100644
index feb911f..0000000
--- a/pkg/build/pipeline_example_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExamplePipeline_Plan references Pipeline.Plan on this package API surface.
-func ExamplePipeline_Plan() {
- _ = (*Pipeline).Plan
- core.Println("Pipeline.Plan")
- // Output: Pipeline.Plan
-}
-
-// ExamplePipeline_Run references Pipeline.Run on this package API surface.
-func ExamplePipeline_Run() {
- _ = (*Pipeline).Run
- core.Println("Pipeline.Run")
- // Output: Pipeline.Run
-}
-
-// ExampleResolveBuildName references ResolveBuildName on this package API surface.
-func ExampleResolveBuildName() {
- _ = ResolveBuildName
- core.Println("ResolveBuildName")
- // Output: ResolveBuildName
-}
diff --git a/pkg/build/pipeline_test.go b/pkg/build/pipeline_test.go
deleted file mode 100644
index 96ebf29..0000000
--- a/pkg/build/pipeline_test.go
+++ /dev/null
@@ -1,643 +0,0 @@
-package build
-
-import (
- "context"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-type stubPipelineBuilder struct {
- artifacts []Artifact
- lastCfg *Config
- lastTgts []Target
-}
-
-func (b *stubPipelineBuilder) Name() string { return "stub" }
-
-func (b *stubPipelineBuilder) Detect(fs storage.Medium, dir string) core.Result {
- return core.Ok(true)
-}
-
-func (b *stubPipelineBuilder) Build(ctx context.Context, cfg *Config, targets []Target) core.Result {
- b.lastCfg = cfg
- b.lastTgts = append([]Target(nil), targets...)
- return core.Ok(append([]Artifact(nil), b.artifacts...))
-}
-
-func requirePipelineOKResult(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requirePipelinePlan(t *testing.T, result core.Result) *PipelinePlan {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(*PipelinePlan)
-}
-
-func requirePipelineRunResult(t *testing.T, result core.Result) *PipelineResult {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(*PipelineResult)
-}
-
-func requirePipelineError(t *testing.T, result core.Result) string {
- t.Helper()
- if result.OK {
- t.Fatal("expected error")
- }
- return result.Error()
-}
-
-func TestPipeline_Plan_Good(t *testing.T) {
- dir := t.TempDir()
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644))
-
- cfg := DefaultConfig()
- cfg.Project.Binary = "core-demo"
- cfg.Build.Obfuscate = true
- cfg.Build.NSIS = true
- cfg.Build.WebView2 = "embed"
- cfg.Build.BuildTags = []string{"integration"}
- cfg.Targets = []TargetConfig{{OS: "linux", Arch: "amd64"}}
-
- builder := &stubPipelineBuilder{}
- var resolvedTypes []ProjectType
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- resolvedTypes = append(resolvedTypes, projectType)
- return core.Ok(builder)
- },
- ResolveVersion: func(ctx context.Context, projectDir string) core.Result {
- if !stdlibAssertEqual(dir, projectDir) {
- t.Fatalf("want %v, got %v", dir, projectDir)
- }
-
- return core.Ok("v1.2.3")
- },
- }
-
- plan := requirePipelinePlan(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: dir,
- BuildConfig: cfg,
- OutputDir: "artifacts",
- }))
- if !stdlibAssertEqual(dir, plan.ProjectDir) {
- t.Fatalf("want %v, got %v", dir, plan.ProjectDir)
- }
- if !stdlibAssertEqual(ProjectTypeWails, plan.ProjectType) {
- t.Fatalf("want %v, got %v", ProjectTypeWails, plan.ProjectType)
- }
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, plan.ProjectTypes) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, plan.ProjectTypes)
- }
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, resolvedTypes) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, resolvedTypes)
- }
- if !stdlibAssertEqual("core-demo", plan.BuildName) {
- t.Fatalf("want %v, got %v", "core-demo", plan.BuildName)
- }
- if !stdlibAssertEqual(ax.Join(dir, "artifacts"), plan.OutputDir) {
- t.Fatalf("want %v, got %v", ax.Join(dir, "artifacts"), plan.OutputDir)
- }
- if !stdlibAssertEqual("v1.2.3", plan.Version) {
- t.Fatalf("want %v, got %v", "v1.2.3", plan.Version)
- }
- if stdlibAssertNil(plan.Discovery) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("wails2", plan.SetupPlan.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "wails2", plan.SetupPlan.PrimaryStackSuggestion)
- }
- if !stdlibAssertEqual([]Target{{OS: "linux", Arch: "amd64"}}, plan.Targets) {
- t.Fatalf("want %v, got %v", []Target{{OS: "linux", Arch: "amd64"}}, plan.Targets)
- }
- if !(plan.Options.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(plan.Options.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("embed", plan.Options.WebView2) {
- t.Fatalf("want %v, got %v", "embed", plan.Options.WebView2)
- }
- if !stdlibAssertContains(plan.Options.Tags, "integration") {
- t.Fatalf("expected %v to contain %v", plan.Options.Tags, "integration")
- }
- if !stdlibAssertEqual("core-demo", plan.RuntimeConfig.Name) {
- t.Fatalf("want %v, got %v", "core-demo", plan.RuntimeConfig.Name)
- }
- if !stdlibAssertEqual(plan.OutputDir, plan.RuntimeConfig.OutputDir) {
- t.Fatalf("want %v, got %v", plan.OutputDir, plan.RuntimeConfig.OutputDir)
- }
- if !stdlibAssertEqual("v1.2.3", plan.RuntimeConfig.Version) {
- t.Fatalf("want %v, got %v", "v1.2.3", plan.RuntimeConfig.Version)
- }
- if !(plan.RuntimeConfig.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(plan.RuntimeConfig.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("embed", plan.RuntimeConfig.WebView2) {
- t.Fatalf("want %v, got %v", "embed", plan.RuntimeConfig.WebView2)
- }
- if !stdlibAssertContains(plan.RuntimeConfig.BuildTags, "integration") {
- t.Fatalf("expected %v to contain %v", plan.RuntimeConfig.BuildTags, "integration")
- }
-
-}
-
-func TestPipeline_Plan_UsesExplicitBuildTypeOverride_Good(t *testing.T) {
- dir := t.TempDir()
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
-
- cfg := DefaultConfig()
- cfg.Build.Type = "go"
-
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- if !stdlibAssertEqual(ProjectTypeNode, projectType) {
- t.Fatalf("want %v, got %v", ProjectTypeNode, projectType)
- }
-
- return core.Ok(&stubPipelineBuilder{})
- },
- }
-
- plan := requirePipelinePlan(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: dir,
- BuildConfig: cfg,
- BuildType: "NoDe",
- Targets: []Target{{OS: "darwin", Arch: "arm64"}},
- }))
- if !stdlibAssertEqual(ProjectTypeNode, plan.ProjectType) {
- t.Fatalf("want %v, got %v", ProjectTypeNode, plan.ProjectType)
- }
- if !stdlibAssertEqual("node", plan.BuildConfig.Build.Type) {
- t.Fatalf("want %v, got %v", "node", plan.BuildConfig.Build.Type)
- }
- if !stdlibAssertEqual("node", plan.SetupPlan.PrimaryStack) {
- t.Fatalf("want %v, got %v", "node", plan.SetupPlan.PrimaryStack)
- }
- if !stdlibAssertEqual("node", plan.SetupPlan.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "node", plan.SetupPlan.PrimaryStackSuggestion)
- }
- if !stdlibAssertContains(setupTools(plan.SetupPlan), SetupToolNode) {
- t.Fatalf("expected %v to contain %v", setupTools(plan.SetupPlan), SetupToolNode)
- }
- if !stdlibAssertEqual([]Target{{OS: "darwin", Arch: "arm64"}}, plan.Targets) {
- t.Fatalf("want %v, got %v", []Target{{OS: "darwin", Arch: "arm64"}}, plan.Targets)
- }
-
-}
-
-func TestPipeline_Plan_NormalisesConfiguredBuildType_Good(t *testing.T) {
- cfg := DefaultConfig()
- cfg.Build.Type = "WaIlS"
-
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- if !stdlibAssertEqual(ProjectTypeWails, projectType) {
- t.Fatalf("want %v, got %v", ProjectTypeWails, projectType)
- }
-
- return core.Ok(&stubPipelineBuilder{})
- },
- }
-
- plan := requirePipelinePlan(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: t.TempDir(),
- BuildConfig: cfg,
- Targets: []Target{{OS: "darwin", Arch: "arm64"}},
- }))
- if !stdlibAssertEqual(ProjectTypeWails, plan.ProjectType) {
- t.Fatalf("want %v, got %v", ProjectTypeWails, plan.ProjectType)
- }
- if !stdlibAssertEqual("wails", plan.BuildConfig.Build.Type) {
- t.Fatalf("want %v, got %v", "wails", plan.BuildConfig.Build.Type)
- }
- if !stdlibAssertEqual("wails2", plan.SetupPlan.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "wails2", plan.SetupPlan.PrimaryStackSuggestion)
- }
- if !stdlibAssertContains(setupTools(plan.SetupPlan), SetupToolWails) {
- t.Fatalf("expected %v to contain %v", setupTools(plan.SetupPlan), SetupToolWails)
- }
- if !stdlibAssertContains(setupTools(plan.SetupPlan), SetupToolNode) {
- t.Fatalf("expected %v to contain %v", setupTools(plan.SetupPlan), SetupToolNode)
- }
-
-}
-
-func TestPipeline_Plan_AppliesActionStyleOverrides_Good(t *testing.T) {
- dir := t.TempDir()
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644))
-
- cfg := DefaultConfig()
- cfg.Build.BuildTags = []string{"integration"}
-
- var resolvedTypes []ProjectType
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- resolvedTypes = append(resolvedTypes, projectType)
- return core.Ok(&stubPipelineBuilder{})
- },
- }
-
- plan := requirePipelinePlan(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: dir,
- BuildConfig: cfg,
- BuildTags: []string{"mlx", "release", "mlx"},
- Obfuscate: true,
- ObfuscateSet: true,
- NSIS: true,
- NSISSet: true,
- WebView2: "download",
- WebView2Set: true,
- DenoBuild: "deno task bundle",
- DenoBuildSet: true,
- BuildCache: true,
- BuildCacheSet: true,
- }))
- if !stdlibAssertContains(plan.Options.Tags, "mlx") {
- t.Fatalf("expected %v to contain %v", plan.Options.Tags, "mlx")
- }
- if !stdlibAssertContains(plan.Options.Tags, "release") {
- t.Fatalf("expected %v to contain %v", plan.Options.Tags, "release")
- }
- if stdlibAssertContains(plan.Options.Tags, "integration") {
- t.Fatalf("expected %v not to contain %v", plan.Options.Tags, "integration")
- }
- if !(plan.Options.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(plan.Options.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("download", plan.Options.WebView2) {
- t.Fatalf("want %v, got %v", "download", plan.Options.WebView2)
- }
- if !stdlibAssertEqual("deno task bundle", plan.BuildConfig.Build.DenoBuild) {
- t.Fatalf("want %v, got %v", "deno task bundle", plan.BuildConfig.Build.DenoBuild)
- }
- if !(plan.BuildConfig.Build.Cache.Enabled) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(ax.Join(dir, ".core", "cache"), plan.BuildConfig.Build.Cache.Directory) {
- t.Fatalf("want %v, got %v", ax.Join(dir, ".core", "cache"), plan.BuildConfig.Build.Cache.Directory)
- }
- if !stdlibAssertEqual([]string{ax.Join(dir, "cache", "go-build"), ax.Join(dir, "cache", "go-mod")}, plan.BuildConfig.Build.Cache.Paths) {
- t.Fatalf("want %v, got %v", []string{ax.Join(dir, "cache", "go-build"), ax.Join(dir, "cache", "go-mod")}, plan.BuildConfig.Build.Cache.Paths)
- }
- if !(plan.RuntimeConfig.Cache.Enabled) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(plan.BuildConfig.Build.Cache.Directory, plan.RuntimeConfig.Cache.Directory) {
- t.Fatalf("want %v, got %v", plan.BuildConfig.Build.Cache.Directory, plan.RuntimeConfig.Cache.Directory)
- }
- if !stdlibAssertEqual(plan.BuildConfig.Build.Cache.Paths, plan.RuntimeConfig.Cache.Paths) {
- t.Fatalf("want %v, got %v", plan.BuildConfig.Build.Cache.Paths, plan.RuntimeConfig.Cache.Paths)
- }
- if !stdlibAssertContains(setupTools(plan.SetupPlan), SetupToolDeno) {
- t.Fatalf("expected %v to contain %v", setupTools(plan.SetupPlan), SetupToolDeno)
- }
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, plan.ProjectTypes) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, plan.ProjectTypes)
- }
- if !stdlibAssertEqual([]ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, resolvedTypes) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode}, resolvedTypes)
- }
-
-}
-
-func TestPipeline_Plan_UsesLocalTargetWhenBuildConfigMissing_Good(t *testing.T) {
- dir := t.TempDir()
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
-
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- if !stdlibAssertEqual(ProjectTypeGo, projectType) {
- t.Fatalf("want %v, got %v", ProjectTypeGo, projectType)
- }
-
- return core.Ok(&stubPipelineBuilder{})
- },
- }
-
- plan := requirePipelinePlan(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: dir,
- BuildType: string(ProjectTypeGo),
- }))
- if !stdlibAssertEqual([]Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}, plan.Targets) {
- t.Fatalf("want %v, got %v", []Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}, plan.Targets)
- }
-
-}
-
-func TestPipeline_Plan_UsesExplicitVersionOverride_Good(t *testing.T) {
- dir := t.TempDir()
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
-
- versionResolverCalled := false
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- if !stdlibAssertEqual(ProjectTypeGo, projectType) {
- t.Fatalf("want %v, got %v", ProjectTypeGo, projectType)
- }
-
- return core.Ok(&stubPipelineBuilder{})
- },
- ResolveVersion: func(ctx context.Context, projectDir string) core.Result {
- versionResolverCalled = true
- return core.Ok("v0.0.1")
- },
- }
-
- plan := requirePipelinePlan(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: dir,
- BuildConfig: DefaultConfig(),
- Version: "v9.9.9",
- Targets: []Target{{OS: "linux", Arch: "amd64"}},
- }))
- if !stdlibAssertEqual("v9.9.9", plan.Version) {
- t.Fatalf("want %v, got %v", "v9.9.9", plan.Version)
- }
- if !stdlibAssertEqual("v9.9.9", plan.RuntimeConfig.Version) {
- t.Fatalf("want %v, got %v", "v9.9.9", plan.RuntimeConfig.Version)
- }
- if versionResolverCalled {
- t.Fatal("expected false")
- }
-
-}
-
-func TestPipeline_Plan_RejectsUnsafeVersionOverride_Bad(t *testing.T) {
- dir := t.TempDir()
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
-
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- return core.Ok(&stubPipelineBuilder{})
- },
- }
-
- err := requirePipelineError(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: dir,
- BuildConfig: DefaultConfig(),
- Version: "v1.2.3 --bad",
- Targets: []Target{{OS: "linux", Arch: "amd64"}},
- }))
- if !stdlibAssertContains(err, "invalid build version override") {
- t.Fatalf("expected %v to contain %v", err, "invalid build version override")
- }
-
-}
-
-func TestPipeline_Plan_DoesNotMutateCallerBuildConfig_Good(t *testing.T) {
- dir := t.TempDir()
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644))
-
- cfg := DefaultConfig()
- cfg.Build.BuildTags = []string{"integration"}
-
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- return core.Ok(&stubPipelineBuilder{})
- },
- }
-
- _ = requirePipelinePlan(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: dir,
- BuildConfig: cfg,
- BuildTags: []string{"mlx"},
- Obfuscate: true,
- ObfuscateSet: true,
- DenoBuild: "deno task bundle",
- DenoBuildSet: true,
- BuildCache: true,
- BuildCacheSet: true,
- }))
- if !stdlibAssertEqual([]string{"integration"}, cfg.Build.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"integration"}, cfg.Build.BuildTags)
- }
- if cfg.Build.Obfuscate {
- t.Fatal("expected false")
- }
- if !stdlibAssertEmpty(cfg.Build.DenoBuild) {
- t.Fatalf("expected empty, got %v", cfg.Build.DenoBuild)
- }
- if cfg.Build.Cache.Enabled {
- t.Fatal("expected false")
- }
- if !stdlibAssertEmpty(cfg.Build.Cache.Directory) {
- t.Fatalf("expected empty, got %v", cfg.Build.Cache.Directory)
- }
- if !stdlibAssertEmpty(cfg.Build.Cache.Paths) {
- t.Fatalf("expected empty, got %v", cfg.Build.Cache.Paths)
- }
-
-}
-
-func TestPipeline_Run_Good(t *testing.T) {
- dir := t.TempDir()
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
-
- builder := &stubPipelineBuilder{
- artifacts: []Artifact{{Path: ax.Join(dir, "dist", "demo"), OS: "linux", Arch: "amd64"}},
- }
-
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- return core.Ok(builder)
- },
- }
-
- plan := requirePipelinePlan(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: dir,
- BuildConfig: DefaultConfig(),
- Targets: []Target{{OS: "linux", Arch: "amd64"}},
- }))
-
- result := requirePipelineRunResult(t, pipeline.Run(context.Background(), plan))
- if !stdlibAssertEqual(plan, result.Plan) {
- t.Fatalf("want %v, got %v", plan, result.Plan)
- }
- if !stdlibAssertEqual([]Artifact{{Path: ax.Join(dir, "dist", "demo"), OS: "linux", Arch: "amd64"}}, result.Artifacts) {
- t.Fatalf("want %v, got %v", []Artifact{{Path: ax.Join(dir, "dist", "demo"), OS: "linux", Arch: "amd64"}}, result.Artifacts)
- }
- if stdlibAssertNil(builder.lastCfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(plan.RuntimeConfig, builder.lastCfg) {
- t.Fatalf("want %v, got %v", plan.RuntimeConfig, builder.lastCfg)
- }
- if !stdlibAssertEqual(plan.Targets, builder.lastTgts) {
- t.Fatalf("want %v, got %v", plan.Targets, builder.lastTgts)
- }
-
-}
-
-func TestPipeline_Run_MultiType_Good(t *testing.T) {
- dir := t.TempDir()
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644))
- requirePipelineOKResult(t, ax.WriteFile(ax.Join(dir, "mkdocs.yml"), []byte("site_name: Demo\n"), 0o644))
-
- nodeBuilder := &stubPipelineBuilder{
- artifacts: []Artifact{{Path: ax.Join(dir, "dist", "node", "linux_amd64", "node-artifact"), OS: "linux", Arch: "amd64"}},
- }
- docsBuilder := &stubPipelineBuilder{
- artifacts: []Artifact{{Path: ax.Join(dir, "dist", "docs", "linux_amd64", "docs-artifact"), OS: "linux", Arch: "amd64"}},
- }
-
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- switch projectType {
- case ProjectTypeNode:
- return core.Ok(nodeBuilder)
- case ProjectTypeDocs:
- return core.Ok(docsBuilder)
- default:
- return core.Fail(core.NewError("test error"))
- }
- },
- }
-
- plan := requirePipelinePlan(t, pipeline.Plan(context.Background(), PipelineRequest{
- ProjectDir: dir,
- BuildConfig: DefaultConfig(),
- Targets: []Target{{OS: "linux", Arch: "amd64"}},
- }))
- if !stdlibAssertEqual([]ProjectType{ProjectTypeNode, ProjectTypeDocs}, plan.ProjectTypes) {
- t.Fatalf("want %v, got %v", []ProjectType{ProjectTypeNode, ProjectTypeDocs}, plan.ProjectTypes)
- }
-
- result := requirePipelineRunResult(t, pipeline.Run(context.Background(), plan))
- if len(result.Artifacts) != 2 {
- t.Fatalf("want len %v, got %v", 2, len(result.Artifacts))
- }
- if stdlibAssertNil(nodeBuilder.lastCfg) {
- t.Fatal("expected non-nil")
- }
- if stdlibAssertNil(docsBuilder.lastCfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(ax.Join(plan.OutputDir, "node"), nodeBuilder.lastCfg.OutputDir) {
- t.Fatalf("want %v, got %v", ax.Join(plan.OutputDir, "node"), nodeBuilder.lastCfg.OutputDir)
- }
- if !stdlibAssertEqual(ax.Join(plan.OutputDir, "docs"), docsBuilder.lastCfg.OutputDir) {
- t.Fatalf("want %v, got %v", ax.Join(plan.OutputDir, "docs"), docsBuilder.lastCfg.OutputDir)
- }
- if !stdlibAssertEqual(plan.Targets, nodeBuilder.lastTgts) {
- t.Fatalf("want %v, got %v", plan.Targets, nodeBuilder.lastTgts)
- }
- if !stdlibAssertEqual(plan.Targets, docsBuilder.lastTgts) {
- t.Fatalf("want %v, got %v", plan.Targets, docsBuilder.lastTgts)
- }
- if plan.RuntimeConfig == nodeBuilder.lastCfg {
- t.Fatalf("expected %v and %v not to be the same", plan.RuntimeConfig, nodeBuilder.lastCfg)
- }
- if plan.RuntimeConfig == docsBuilder.lastCfg {
- t.Fatalf("expected %v and %v not to be the same", plan.RuntimeConfig, docsBuilder.lastCfg)
- }
-
-}
-
-func TestPipeline_Plan_Bad(t *testing.T) {
- pipeline := &Pipeline{
- FS: storage.Local,
- ResolveBuilder: func(projectType ProjectType) core.Result {
- return core.Ok(&stubPipelineBuilder{})
- },
- }
-
- err := requirePipelineError(t, pipeline.Plan(context.Background(), PipelineRequest{ProjectDir: t.TempDir()}))
- if !stdlibAssertContains(err, "no buildable project type found") {
- t.Fatalf("expected %v to contain %v", err, "no buildable project type found")
- }
-
-}
-
-func TestPipeline_Run_Bad(t *testing.T) {
- pipeline := &Pipeline{}
-
- err := requirePipelineError(t, pipeline.Run(context.Background(), nil))
- if !stdlibAssertContains(err, "pipeline plan is nil") {
- t.Fatalf("expected %v to contain %v", err, "pipeline plan is nil")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestPipeline_Pipeline_Plan_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &Pipeline{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Plan(ctx, PipelineRequest{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestPipeline_Pipeline_Run_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &Pipeline{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Run(ctx, &PipelinePlan{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestPipeline_ResolveBuildName_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveBuildName(core.Path(t.TempDir(), "go-build-compliance"), &BuildConfig{}, "agent")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestPipeline_ResolveBuildName_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveBuildName("", nil, "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestPipeline_ResolveBuildName_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveBuildName(core.Path(t.TempDir(), "go-build-compliance"), &BuildConfig{}, "agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/run.go b/pkg/build/run.go
deleted file mode 100644
index 82021ba..0000000
--- a/pkg/build/run.go
+++ /dev/null
@@ -1,422 +0,0 @@
-package build
-
-import (
- "context"
- "io/fs"
- "reflect"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-var defaultBuilderResolver BuilderResolver
-
-// RunConfig captures the option-style inputs for the RFC-documented build API.
-type RunConfig struct {
- Context context.Context
- ProjectDir string
- ConfigPath string
- BuildConfig *BuildConfig
- BuildType string
- BuildTags []string
- Obfuscate bool
- ObfuscateSet bool
- NSIS bool
- NSISSet bool
- WebView2 string
- WebView2Set bool
- DenoBuild string
- DenoBuildSet bool
- NpmBuild string
- NpmBuildSet bool
- BuildCache bool
- BuildCacheSet bool
- BuildName string
- OutputDir string
- Output coreio.Medium
- Targets []Target
- Version string
- ResolveBuilder BuilderResolver
- ResolveVersion VersionResolver
-}
-
-// RunOption mutates a RunConfig before the pipeline executes.
-type RunOption func(*RunConfig)
-
-// RegisterDefaultBuilderResolver installs the builder resolver used by Run when
-// the caller does not provide one explicitly.
-func RegisterDefaultBuilderResolver(resolver BuilderResolver) {
- defaultBuilderResolver = resolver
-}
-
-// DefaultBuilderResolver returns the currently registered default builder resolver.
-func DefaultBuilderResolver() BuilderResolver {
- return defaultBuilderResolver
-}
-
-// DefaultRunConfig returns the default configuration for the option-style Run API.
-func DefaultRunConfig() *RunConfig {
- return &RunConfig{
- Context: context.Background(),
- Output: coreio.Local,
- }
-}
-
-// WithContext overrides the context used for discovery, versioning, and builds.
-func WithContext(ctx context.Context) RunOption {
- return func(cfg *RunConfig) {
- cfg.Context = ctx
- }
-}
-
-// WithProjectDir sets the project directory to build.
-func WithProjectDir(dir string) RunOption {
- return func(cfg *RunConfig) {
- cfg.ProjectDir = dir
- }
-}
-
-// WithConfigPath points Run at an explicit build config file.
-func WithConfigPath(path string) RunOption {
- return func(cfg *RunConfig) {
- cfg.ConfigPath = path
- }
-}
-
-// WithBuildConfig injects a preloaded build config instead of loading .core/build.yaml.
-func WithBuildConfig(buildConfig *BuildConfig) RunOption {
- return func(cfg *RunConfig) {
- cfg.BuildConfig = buildConfig
- }
-}
-
-// WithBuildType forces a specific project type instead of auto-detection.
-func WithBuildType(buildType string) RunOption {
- return func(cfg *RunConfig) {
- cfg.BuildType = buildType
- }
-}
-
-// WithBuildTags overrides the Go build tags passed through the pipeline.
-func WithBuildTags(tags ...string) RunOption {
- return func(cfg *RunConfig) {
- cfg.BuildTags = append([]string(nil), tags...)
- }
-}
-
-// WithObfuscate enables or disables garble-backed obfuscation for the build.
-func WithObfuscate(enabled bool) RunOption {
- return func(cfg *RunConfig) {
- cfg.Obfuscate = enabled
- cfg.ObfuscateSet = true
- }
-}
-
-// WithNSIS enables or disables Windows NSIS installer generation for Wails builds.
-func WithNSIS(enabled bool) RunOption {
- return func(cfg *RunConfig) {
- cfg.NSIS = enabled
- cfg.NSISSet = true
- }
-}
-
-// WithWebView2 sets the Wails WebView2 delivery mode: download, embed, browser, or error.
-func WithWebView2(mode string) RunOption {
- return func(cfg *RunConfig) {
- cfg.WebView2 = mode
- cfg.WebView2Set = true
- }
-}
-
-// WithDenoBuild overrides the default Deno frontend build command.
-func WithDenoBuild(command string) RunOption {
- return func(cfg *RunConfig) {
- cfg.DenoBuild = command
- cfg.DenoBuildSet = true
- }
-}
-
-// WithNpmBuild overrides the default npm frontend build command.
-func WithNpmBuild(command string) RunOption {
- return func(cfg *RunConfig) {
- cfg.NpmBuild = command
- cfg.NpmBuildSet = true
- }
-}
-
-// WithBuildCache enables or disables build cache setup before the pipeline runs.
-func WithBuildCache(enabled bool) RunOption {
- return func(cfg *RunConfig) {
- cfg.BuildCache = enabled
- cfg.BuildCacheSet = true
- }
-}
-
-// WithBuildName overrides the resolved artifact name.
-func WithBuildName(name string) RunOption {
- return func(cfg *RunConfig) {
- cfg.BuildName = name
- }
-}
-
-// WithOutputDir sets the destination directory or key prefix for mirrored artifacts.
-func WithOutputDir(dir string) RunOption {
- return func(cfg *RunConfig) {
- cfg.OutputDir = dir
- }
-}
-
-// WithOutput sets the destination medium used for final build artifacts.
-func WithOutput(output coreio.Medium) RunOption {
- return func(cfg *RunConfig) {
- cfg.Output = output
- }
-}
-
-// WithTargets overrides the build matrix targets.
-func WithTargets(targets ...Target) RunOption {
- return func(cfg *RunConfig) {
- cfg.Targets = append([]Target(nil), targets...)
- }
-}
-
-// WithVersion overrides the resolved build version.
-func WithVersion(version string) RunOption {
- return func(cfg *RunConfig) {
- cfg.Version = version
- }
-}
-
-// WithBuilderResolver provides an explicit builder resolver for Run.
-func WithBuilderResolver(resolver BuilderResolver) RunOption {
- return func(cfg *RunConfig) {
- cfg.ResolveBuilder = resolver
- }
-}
-
-// WithVersionResolver provides an explicit version resolver for Run.
-func WithVersionResolver(resolver VersionResolver) RunOption {
- return func(cfg *RunConfig) {
- cfg.ResolveVersion = resolver
- }
-}
-
-// Run executes the build pipeline and mirrors produced artifacts into the
-// configured output medium.
-//
-// result := build.Run(build.WithOutput(io.Local))
-func Run(opts ...RunOption) core.Result {
- cfg := DefaultRunConfig()
- for _, opt := range opts {
- if opt != nil {
- opt(cfg)
- }
- }
-
- ctx := cfg.Context
- if ctx == nil {
- ctx = context.Background()
- }
-
- projectDir := cfg.ProjectDir
- if projectDir == "" {
- wd := ax.Getwd()
- if !wd.OK {
- return core.Fail(core.E("build.Run", "failed to get working directory", core.NewError(wd.Error())))
- }
- projectDir = wd.Value.(string)
- }
- projectDir = ax.Clean(projectDir)
-
- output := cfg.Output
- if output == nil {
- output = coreio.Local
- }
-
- destinationRoot := resolveRunOutputRoot(projectDir, cfg.OutputDir, output)
-
- stage := ax.MkdirTemp("core-build-*")
- if !stage.OK {
- return core.Fail(core.E("build.Run", "failed to create build staging directory", core.NewError(stage.Error())))
- }
- stageRoot := stage.Value.(string)
- defer ax.RemoveAll(stageRoot)
-
- stageOutputDir := ax.Join(stageRoot, "dist")
-
- resolver := cfg.ResolveBuilder
- if resolver == nil {
- resolver = DefaultBuilderResolver()
- }
- if resolver == nil {
- resolver = resolveBuiltinBuilder
- }
-
- pipeline := &Pipeline{
- FS: coreio.Local,
- ResolveBuilder: resolver,
- ResolveVersion: cfg.ResolveVersion,
- }
-
- planResult := pipeline.Plan(ctx, PipelineRequest{
- ProjectDir: projectDir,
- ConfigPath: cfg.ConfigPath,
- BuildConfig: cfg.BuildConfig,
- BuildType: cfg.BuildType,
- BuildTags: append([]string(nil), cfg.BuildTags...),
- Obfuscate: cfg.Obfuscate,
- ObfuscateSet: cfg.ObfuscateSet,
- NSIS: cfg.NSIS,
- NSISSet: cfg.NSISSet,
- WebView2: cfg.WebView2,
- WebView2Set: cfg.WebView2Set,
- DenoBuild: cfg.DenoBuild,
- DenoBuildSet: cfg.DenoBuildSet,
- NpmBuild: cfg.NpmBuild,
- NpmBuildSet: cfg.NpmBuildSet,
- BuildCache: cfg.BuildCache,
- BuildCacheSet: cfg.BuildCacheSet,
- OutputDir: stageOutputDir,
- BuildName: cfg.BuildName,
- Targets: append([]Target(nil), cfg.Targets...),
- Version: cfg.Version,
- })
- if !planResult.OK {
- return planResult
- }
- plan := planResult.Value.(*PipelinePlan)
-
- result := pipeline.Run(ctx, plan)
- if !result.OK {
- return result
- }
- pipelineResult := result.Value.(*PipelineResult)
-
- return mirrorArtifacts(coreio.Local, output, stageOutputDir, destinationRoot, pipelineResult.Artifacts)
-}
-
-func resolveRunOutputRoot(projectDir, outputDir string, output coreio.Medium) string {
- if outputDir == "" && !mediumEquals(output, coreio.Local) {
- return ""
- }
-
- if outputDir == "" {
- outputDir = "dist"
- }
-
- if !ax.IsAbs(outputDir) && mediumEquals(output, coreio.Local) {
- return ax.Join(projectDir, outputDir)
- }
-
- return outputDir
-}
-
-func mediumEquals(left, right coreio.Medium) bool {
- if left == nil || right == nil {
- return left == nil && right == nil
- }
-
- leftType := reflect.TypeOf(left)
- rightType := reflect.TypeOf(right)
- if leftType != rightType || !leftType.Comparable() {
- return false
- }
-
- return reflect.ValueOf(left).Interface() == reflect.ValueOf(right).Interface()
-}
-
-func mirrorArtifacts(source, destination coreio.Medium, sourceRoot, destinationRoot string, artifacts []Artifact) core.Result {
- if source == nil {
- source = coreio.Local
- }
- if destination == nil {
- destination = coreio.Local
- }
-
- mirrored := make([]Artifact, 0, len(artifacts))
- for _, artifact := range artifacts {
- relativePathResult := ax.Rel(sourceRoot, artifact.Path)
- relativePath := ""
- if relativePathResult.OK {
- relativePath = relativePathResult.Value.(string)
- }
- if !relativePathResult.OK || relativePath == "" || core.HasPrefix(relativePath, "..") {
- relativePath = ax.Base(artifact.Path)
- }
-
- destinationPath := joinOutputPath(destinationRoot, relativePath)
- copied := copyMediumPath(source, artifact.Path, destination, destinationPath)
- if !copied.OK {
- return core.Fail(core.E("build.Run", "failed to mirror artifact "+artifact.Path, core.NewError(copied.Error())))
- }
-
- mirroredArtifact := artifact
- mirroredArtifact.Path = destinationPath
- mirrored = append(mirrored, mirroredArtifact)
- }
-
- return core.Ok(mirrored)
-}
-
-func joinOutputPath(root, path string) string {
- if root == "" || root == "." {
- return ax.Clean(path)
- }
- if path == "" || path == "." {
- return ax.Clean(root)
- }
- return ax.Join(root, path)
-}
-
-func copyMediumPath(source coreio.Medium, sourcePath string, destination coreio.Medium, destinationPath string) core.Result {
- infoResult := source.Stat(sourcePath)
- if !infoResult.OK {
- return infoResult
- }
- info := infoResult.Value.(fs.FileInfo)
-
- if info.IsDir() {
- return copyMediumDir(source, sourcePath, destination, destinationPath)
- }
-
- return copyMediumFile(source, sourcePath, destination, destinationPath, info.Mode())
-}
-
-func copyMediumDir(source coreio.Medium, sourcePath string, destination coreio.Medium, destinationPath string) core.Result {
- created := destination.EnsureDir(destinationPath)
- if !created.OK {
- return created
- }
-
- entriesResult := source.List(sourcePath)
- if !entriesResult.OK {
- return entriesResult
- }
- entries := entriesResult.Value.([]fs.DirEntry)
-
- for _, entry := range entries {
- childSourcePath := ax.Join(sourcePath, entry.Name())
- childDestinationPath := ax.Join(destinationPath, entry.Name())
- copied := copyMediumPath(source, childSourcePath, destination, childDestinationPath)
- if !copied.OK {
- return copied
- }
- }
-
- return core.Ok(nil)
-}
-
-func copyMediumFile(source coreio.Medium, sourcePath string, destination coreio.Medium, destinationPath string, mode fs.FileMode) core.Result {
- created := destination.EnsureDir(ax.Dir(destinationPath))
- if !created.OK {
- return created
- }
-
- content := source.Read(sourcePath)
- if !content.OK {
- return content
- }
-
- return destination.WriteMode(destinationPath, content.Value.(string), mode)
-}
diff --git a/pkg/build/run_example_test.go b/pkg/build/run_example_test.go
deleted file mode 100644
index 2cab068..0000000
--- a/pkg/build/run_example_test.go
+++ /dev/null
@@ -1,164 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleRegisterDefaultBuilderResolver references RegisterDefaultBuilderResolver on this package API surface.
-func ExampleRegisterDefaultBuilderResolver() {
- _ = RegisterDefaultBuilderResolver
- core.Println("RegisterDefaultBuilderResolver")
- // Output: RegisterDefaultBuilderResolver
-}
-
-// ExampleDefaultBuilderResolver references DefaultBuilderResolver on this package API surface.
-func ExampleDefaultBuilderResolver() {
- _ = DefaultBuilderResolver
- core.Println("DefaultBuilderResolver")
- // Output: DefaultBuilderResolver
-}
-
-// ExampleDefaultRunConfig references DefaultRunConfig on this package API surface.
-func ExampleDefaultRunConfig() {
- _ = DefaultRunConfig
- core.Println("DefaultRunConfig")
- // Output: DefaultRunConfig
-}
-
-// ExampleWithContext references WithContext on this package API surface.
-func ExampleWithContext() {
- _ = WithContext
- core.Println("WithContext")
- // Output: WithContext
-}
-
-// ExampleWithProjectDir references WithProjectDir on this package API surface.
-func ExampleWithProjectDir() {
- _ = WithProjectDir
- core.Println("WithProjectDir")
- // Output: WithProjectDir
-}
-
-// ExampleWithConfigPath references WithConfigPath on this package API surface.
-func ExampleWithConfigPath() {
- _ = WithConfigPath
- core.Println("WithConfigPath")
- // Output: WithConfigPath
-}
-
-// ExampleWithBuildConfig references WithBuildConfig on this package API surface.
-func ExampleWithBuildConfig() {
- _ = WithBuildConfig
- core.Println("WithBuildConfig")
- // Output: WithBuildConfig
-}
-
-// ExampleWithBuildType references WithBuildType on this package API surface.
-func ExampleWithBuildType() {
- _ = WithBuildType
- core.Println("WithBuildType")
- // Output: WithBuildType
-}
-
-// ExampleWithBuildTags references WithBuildTags on this package API surface.
-func ExampleWithBuildTags() {
- _ = WithBuildTags
- core.Println("WithBuildTags")
- // Output: WithBuildTags
-}
-
-// ExampleWithObfuscate references WithObfuscate on this package API surface.
-func ExampleWithObfuscate() {
- _ = WithObfuscate
- core.Println("WithObfuscate")
- // Output: WithObfuscate
-}
-
-// ExampleWithNSIS references WithNSIS on this package API surface.
-func ExampleWithNSIS() {
- _ = WithNSIS
- core.Println("WithNSIS")
- // Output: WithNSIS
-}
-
-// ExampleWithWebView2 references WithWebView2 on this package API surface.
-func ExampleWithWebView2() {
- _ = WithWebView2
- core.Println("WithWebView2")
- // Output: WithWebView2
-}
-
-// ExampleWithDenoBuild references WithDenoBuild on this package API surface.
-func ExampleWithDenoBuild() {
- _ = WithDenoBuild
- core.Println("WithDenoBuild")
- // Output: WithDenoBuild
-}
-
-// ExampleWithNpmBuild references WithNpmBuild on this package API surface.
-func ExampleWithNpmBuild() {
- _ = WithNpmBuild
- core.Println("WithNpmBuild")
- // Output: WithNpmBuild
-}
-
-// ExampleWithBuildCache references WithBuildCache on this package API surface.
-func ExampleWithBuildCache() {
- _ = WithBuildCache
- core.Println("WithBuildCache")
- // Output: WithBuildCache
-}
-
-// ExampleWithBuildName references WithBuildName on this package API surface.
-func ExampleWithBuildName() {
- _ = WithBuildName
- core.Println("WithBuildName")
- // Output: WithBuildName
-}
-
-// ExampleWithOutputDir references WithOutputDir on this package API surface.
-func ExampleWithOutputDir() {
- _ = WithOutputDir
- core.Println("WithOutputDir")
- // Output: WithOutputDir
-}
-
-// ExampleWithOutput references WithOutput on this package API surface.
-func ExampleWithOutput() {
- _ = WithOutput
- core.Println("WithOutput")
- // Output: WithOutput
-}
-
-// ExampleWithTargets references WithTargets on this package API surface.
-func ExampleWithTargets() {
- _ = WithTargets
- core.Println("WithTargets")
- // Output: WithTargets
-}
-
-// ExampleWithVersion references WithVersion on this package API surface.
-func ExampleWithVersion() {
- _ = WithVersion
- core.Println("WithVersion")
- // Output: WithVersion
-}
-
-// ExampleWithBuilderResolver references WithBuilderResolver on this package API surface.
-func ExampleWithBuilderResolver() {
- _ = WithBuilderResolver
- core.Println("WithBuilderResolver")
- // Output: WithBuilderResolver
-}
-
-// ExampleWithVersionResolver references WithVersionResolver on this package API surface.
-func ExampleWithVersionResolver() {
- _ = WithVersionResolver
- core.Println("WithVersionResolver")
- // Output: WithVersionResolver
-}
-
-// ExampleRun references Run on this package API surface.
-func ExampleRun() {
- _ = Run
- core.Println("Run")
- // Output: Run
-}
diff --git a/pkg/build/run_test.go b/pkg/build/run_test.go
deleted file mode 100644
index 3793c26..0000000
--- a/pkg/build/run_test.go
+++ /dev/null
@@ -1,957 +0,0 @@
-package build
-
-import (
- "context"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-type runTestBuilder struct {
- directoryArtifact bool
-}
-
-type capturingRunTestBuilder struct {
- captured **Config
-}
-
-func (b *runTestBuilder) Name() string { return "run-test" }
-
-func (b *runTestBuilder) Detect(fs coreio.Medium, dir string) core.Result {
- return core.Ok(true)
-}
-
-func (b *runTestBuilder) Build(ctx context.Context, cfg *Config, targets []Target) core.Result {
- if cfg.FS == nil {
- cfg.FS = coreio.Local
- }
- if len(targets) == 0 {
- targets = []Target{{OS: "linux", Arch: "amd64"}}
- }
-
- artifacts := make([]Artifact, 0, len(targets))
- for _, target := range targets {
- basePath := ax.Join(cfg.OutputDir, target.OS+"_"+target.Arch, cfg.Name)
- if b.directoryArtifact {
- artifactPath := basePath + ".app"
- created := cfg.FS.EnsureDir(ax.Join(artifactPath, "Contents", "MacOS"))
- if !created.OK {
- return created
- }
- written := cfg.FS.WriteMode(ax.Join(artifactPath, "Contents", "MacOS", cfg.Name), "bundle:"+target.String(), 0o755)
- if !written.OK {
- return written
- }
- artifacts = append(artifacts, Artifact{Path: artifactPath, OS: target.OS, Arch: target.Arch})
- continue
- }
-
- created := cfg.FS.EnsureDir(ax.Dir(basePath))
- if !created.OK {
- return created
- }
- written := cfg.FS.WriteMode(basePath, "artifact:"+target.String(), 0o755)
- if !written.OK {
- return written
- }
- artifacts = append(artifacts, Artifact{Path: basePath, OS: target.OS, Arch: target.Arch})
- }
-
- return core.Ok(artifacts)
-}
-
-func (b *capturingRunTestBuilder) Name() string { return "capturing-run-test" }
-
-func (b *capturingRunTestBuilder) Detect(fs coreio.Medium, dir string) core.Result {
- return core.Ok(true)
-}
-
-func (b *capturingRunTestBuilder) Build(ctx context.Context, cfg *Config, targets []Target) core.Result {
- if b.captured != nil {
- *b.captured = cfg
- }
- return (&runTestBuilder{}).Build(ctx, cfg, targets)
-}
-
-func requireRunOKResult(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requireRunArtifacts(t *testing.T, result core.Result) []Artifact {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]Artifact)
-}
-
-func requireRunString(t *testing.T, result core.Result) string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func requireRunError(t *testing.T, result core.Result) string {
- t.Helper()
- if result.OK {
- t.Fatal("expected error")
- }
- return result.Error()
-}
-
-func TestRun_UsesOutputMediumGood(t *testing.T) {
- projectDir := t.TempDir()
- output := coreio.NewMemoryMedium()
-
- artifacts := requireRunArtifacts(t, Run(
- WithContext(context.Background()),
- WithProjectDir(projectDir),
- WithBuildConfig(DefaultConfig()),
- WithBuildType(string(ProjectTypeGo)),
- WithBuildName("core-build"),
- WithTargets(Target{OS: "linux", Arch: "amd64"}),
- WithOutput(output),
- WithOutputDir("releases"),
- WithBuilderResolver(func(projectType ProjectType) core.Result {
- return core.Ok(&runTestBuilder{})
- }),
- ))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(ax.Join("releases", "linux_amd64", "core-build"), artifacts[0].Path) {
- t.Fatalf("want %v, got %v", ax.Join("releases", "linux_amd64", "core-build"), artifacts[0].Path)
- }
-
- content := requireRunString(t, output.Read(ax.Join("releases", "linux_amd64", "core-build")))
- if !stdlibAssertEqual("artifact:linux/amd64", content) {
- t.Fatalf("want %v, got %v", "artifact:linux/amd64", content)
- }
-
-}
-
-func TestRun_UsesOutputMediumRootWhenOutputDirUnsetGood(t *testing.T) {
- projectDir := t.TempDir()
- output := coreio.NewMemoryMedium()
-
- artifacts := requireRunArtifacts(t, Run(
- WithContext(context.Background()),
- WithProjectDir(projectDir),
- WithBuildConfig(DefaultConfig()),
- WithBuildType(string(ProjectTypeGo)),
- WithBuildName("core-build"),
- WithTargets(Target{OS: "linux", Arch: "amd64"}),
- WithOutput(output),
- WithBuilderResolver(func(projectType ProjectType) core.Result {
- return core.Ok(&runTestBuilder{})
- }),
- ))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
- if !stdlibAssertEqual(ax.Join("linux_amd64", "core-build"), artifacts[0].Path) {
- t.Fatalf("want %v, got %v", ax.Join("linux_amd64", "core-build"), artifacts[0].Path)
- }
-
- content := requireRunString(t, output.Read(ax.Join("linux_amd64", "core-build")))
- if !stdlibAssertEqual("artifact:linux/amd64", content) {
- t.Fatalf("want %v, got %v", "artifact:linux/amd64", content)
- }
-
-}
-
-func TestRun_MirrorsDirectoryArtifactsGood(t *testing.T) {
- projectDir := t.TempDir()
- output := coreio.NewMemoryMedium()
-
- artifacts := requireRunArtifacts(t, Run(
- WithProjectDir(projectDir),
- WithBuildConfig(DefaultConfig()),
- WithBuildType(string(ProjectTypeWails)),
- WithBuildName("core-build"),
- WithTargets(Target{OS: "darwin", Arch: "arm64"}),
- WithOutput(output),
- WithOutputDir("bundles"),
- WithBuilderResolver(func(projectType ProjectType) core.Result {
- return core.Ok(&runTestBuilder{directoryArtifact: true})
- }),
- ))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- bundlePath := ax.Join("bundles", "darwin_arm64", "core-build.app")
- if !stdlibAssertEqual(bundlePath, artifacts[0].Path) {
- t.Fatalf("want %v, got %v", bundlePath, artifacts[0].Path)
- }
- if !(output.IsDir(bundlePath)) {
- t.Fatal("expected true")
- }
-
- binaryPath := ax.Join(bundlePath, "Contents", "MacOS", "core-build")
- content := requireRunString(t, output.Read(binaryPath))
- if !stdlibAssertEqual("bundle:darwin/arm64", content) {
- t.Fatalf("want %v, got %v", "bundle:darwin/arm64", content)
- }
-
-}
-
-func TestRun_UsesLocalTargetWhenBuildConfigMissingGood(t *testing.T) {
- projectDir := t.TempDir()
- output := coreio.NewMemoryMedium()
- requireRunOKResult(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/demo\n"), 0o644))
-
- artifacts := requireRunArtifacts(t, Run(
- WithProjectDir(projectDir),
- WithBuildType(string(ProjectTypeGo)),
- WithBuildName("core-build"),
- WithOutput(output),
- WithOutputDir("releases"),
- WithBuilderResolver(func(projectType ProjectType) core.Result {
- return core.Ok(&runTestBuilder{})
- }),
- ))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- expectedPath := ax.Join("releases", runtime.GOOS+"_"+runtime.GOARCH, "core-build")
- if !stdlibAssertEqual(expectedPath, artifacts[0].Path) {
- t.Fatalf("want %v, got %v", expectedPath, artifacts[0].Path)
- }
-
-}
-
-func TestRun_UsesBuiltinGoResolverWhenResolverUnsetGood(t *testing.T) {
- projectDir := t.TempDir()
- requireRunOKResult(t, ax.WriteFile(ax.Join(projectDir, "go.mod"), []byte("module example.com/builtin\n\ngo 1.24\n"), 0o644))
- requireRunOKResult(t, ax.WriteFile(ax.Join(projectDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644))
-
- output := coreio.NewMemoryMedium()
- artifacts := requireRunArtifacts(t, Run(
- WithProjectDir(projectDir),
- WithBuildConfig(DefaultConfig()),
- WithBuildType(string(ProjectTypeGo)),
- WithBuildName("core-build"),
- WithTargets(Target{OS: runtime.GOOS, Arch: runtime.GOARCH}),
- WithOutput(output),
- WithOutputDir("releases"),
- ))
- if len(artifacts) != 1 {
- t.Fatalf("want len %v, got %v", 1, len(artifacts))
- }
-
- expectedPath := ax.Join("releases", runtime.GOOS+"_"+runtime.GOARCH, "core-build")
- if runtime.GOOS == "windows" {
- expectedPath += ".exe"
- }
- if !stdlibAssertEqual(expectedPath, artifacts[0].Path) {
- t.Fatalf("want %v, got %v", expectedPath, artifacts[0].Path)
- }
- if !(output.Exists(expectedPath)) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestRun_Bad_NoBuilderResolverForUnsupportedProjectType(t *testing.T) {
- projectDir := t.TempDir()
-
- err := requireRunError(t, Run(
- WithProjectDir(projectDir),
- WithBuildConfig(DefaultConfig()),
- WithBuildType(string(ProjectTypeNode)),
- ))
- if !stdlibAssertContains(err, "builtin fallback only supports go projects") {
- t.Fatalf("expected %v to contain %v", err, "builtin fallback only supports go projects")
- }
-
-}
-
-func TestRun_ForwardsActionPortOverridesGood(t *testing.T) {
- projectDir := t.TempDir()
-
- var captured *Config
- _ = requireRunArtifacts(t, Run(
- WithProjectDir(projectDir),
- WithBuildConfig(DefaultConfig()),
- WithBuildType(string(ProjectTypeGo)),
- WithBuildName("core-build"),
- WithTargets(Target{OS: "linux", Arch: "amd64"}),
- WithBuildTags("integration", "release"),
- WithObfuscate(true),
- WithNSIS(true),
- WithWebView2("embed"),
- WithDenoBuild("deno task bundle"),
- WithNpmBuild("npm run bundle"),
- WithBuildCache(true),
- WithBuilderResolver(func(projectType ProjectType) core.Result {
- return core.Ok(&capturingRunTestBuilder{captured: &captured})
- }),
- WithOutput(coreio.NewMemoryMedium()),
- ))
- if stdlibAssertNil(captured) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual([]string{"integration", "release"}, captured.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"integration", "release"}, captured.BuildTags)
- }
- if !(captured.Obfuscate) {
- t.Fatal("expected true")
- }
- if !(captured.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("embed", captured.WebView2) {
- t.Fatalf("want %v, got %v", "embed", captured.WebView2)
- }
- if !stdlibAssertEqual("deno task bundle", captured.DenoBuild) {
- t.Fatalf("want %v, got %v", "deno task bundle", captured.DenoBuild)
- }
- if !stdlibAssertEqual("npm run bundle", captured.NpmBuild) {
- t.Fatalf("want %v, got %v", "npm run bundle", captured.NpmBuild)
- }
- if !(captured.Cache.Enabled) {
- t.Fatal("expected true")
- }
- if stdlibAssertEmpty(captured.Cache.Paths) {
- t.Fatal("expected non-empty")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestRun_RegisterDefaultBuilderResolver_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- RegisterDefaultBuilderResolver(nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_RegisterDefaultBuilderResolver_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- RegisterDefaultBuilderResolver(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_RegisterDefaultBuilderResolver_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- RegisterDefaultBuilderResolver(nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_DefaultBuilderResolver_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultBuilderResolver()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_DefaultBuilderResolver_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultBuilderResolver()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_DefaultBuilderResolver_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultBuilderResolver()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_DefaultRunConfig_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultRunConfig()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_DefaultRunConfig_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultRunConfig()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_DefaultRunConfig_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = DefaultRunConfig()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithContext_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithContext(ctx)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithContext_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithContext(ctx)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithContext_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithContext(ctx)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithProjectDir_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithProjectDir(core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithProjectDir_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithProjectDir("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithProjectDir_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithProjectDir(core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithConfigPath_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithConfigPath(core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithConfigPath_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithConfigPath("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithConfigPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithConfigPath(core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithBuildConfig_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildConfig(&BuildConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithBuildConfig_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildConfig(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithBuildConfig_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildConfig(&BuildConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithBuildType_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildType("agent")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithBuildType_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildType("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithBuildType_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildType("agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithBuildTags_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildTags()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithBuildTags_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildTags()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithBuildTags_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildTags()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithObfuscate_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithObfuscate(true)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithObfuscate_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithObfuscate(false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithObfuscate_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithObfuscate(true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithNSIS_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithNSIS(true)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithNSIS_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithNSIS(false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithNSIS_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithNSIS(true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithWebView2_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithWebView2("agent")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithWebView2_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithWebView2("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithWebView2_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithWebView2("agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithDenoBuild_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithDenoBuild("dappcore-command-not-found")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithDenoBuild_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithDenoBuild("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithDenoBuild_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithDenoBuild("dappcore-command-not-found")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithNpmBuild_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithNpmBuild("dappcore-command-not-found")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithNpmBuild_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithNpmBuild("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithNpmBuild_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithNpmBuild("dappcore-command-not-found")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithBuildCache_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildCache(true)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithBuildCache_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildCache(false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithBuildCache_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildCache(true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithBuildName_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildName("agent")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithBuildName_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildName("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithBuildName_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuildName("agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithOutputDir_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithOutputDir(core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithOutputDir_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithOutputDir("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithOutputDir_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithOutputDir(core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithOutput_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithOutput(coreio.NewMemoryMedium())
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithOutput_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithOutput(coreio.NewMemoryMedium())
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithOutput_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithOutput(coreio.NewMemoryMedium())
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithTargets_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithTargets()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithTargets_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithTargets()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithTargets_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithTargets()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithVersion_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithVersion("v1.2.3")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithVersion_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithVersion("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithVersion_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithVersion("v1.2.3")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithBuilderResolver_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuilderResolver(nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithBuilderResolver_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuilderResolver(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithBuilderResolver_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithBuilderResolver(nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_WithVersionResolver_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithVersionResolver(nil)
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_WithVersionResolver_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithVersionResolver(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_WithVersionResolver_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WithVersionResolver(nil)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestRun_Run_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Run()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRun_Run_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Run()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRun_Run_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = Run()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/runtime_config.go b/pkg/build/runtime_config.go
deleted file mode 100644
index 384ae5a..0000000
--- a/pkg/build/runtime_config.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package build
-
-import (
- core "dappco.re/go"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// RuntimeConfigFromBuildConfig maps persisted build settings onto a runtime
-// builder config while preserving the caller's output/name/version overrides.
-func RuntimeConfigFromBuildConfig(filesystem storage.Medium, projectDir, outputDir, binaryName string, buildConfig *BuildConfig, push bool, imageName string, version string) *Config {
- if buildConfig == nil {
- buildConfig = DefaultConfig()
- }
-
- buildDefaults := buildConfig.Build
- denoBuild := buildDefaults.DenoBuild
- if denoBuild == "" {
- denoBuild = buildConfig.PreBuild.Deno
- }
- npmBuild := buildDefaults.NpmBuild
- if npmBuild == "" {
- npmBuild = buildConfig.PreBuild.Npm
- }
-
- versionSafe := version == "" || versionIsSafeRelease(version)
-
- ldFlags := append([]string{}, buildDefaults.LDFlags...)
- if version == "" {
- // Preserve template placeholders when no version is being injected.
- } else if versionSafe {
- ldFlags = ExpandVersionTemplates(ldFlags, version)
- } else {
- ldFlags = stripVersionTemplateFlags(ldFlags)
- }
-
- flags := append([]string{}, buildDefaults.Flags...)
- if versionSafe {
- flags = ExpandVersionTemplates(flags, version)
- } else if version != "" {
- flags = stripVersionTemplateValues(flags)
- }
-
- env := append([]string{}, buildDefaults.Env...)
- if versionSafe {
- env = ExpandVersionTemplates(env, version)
- } else if version != "" {
- env = stripVersionTemplateValues(env)
- }
-
- cfg := &Config{
- FS: filesystem,
- Project: buildConfig.Project,
- ProjectDir: projectDir,
- OutputDir: outputDir,
- Name: binaryName,
- Version: version,
- LDFlags: ldFlags,
- Flags: flags,
- BuildTags: append([]string{}, buildDefaults.BuildTags...),
- Env: env,
- Cache: buildDefaults.Cache,
- CGO: buildDefaults.CGO,
- Obfuscate: buildDefaults.Obfuscate,
- DenoBuild: denoBuild,
- NpmBuild: npmBuild,
- NSIS: buildDefaults.NSIS,
- WebView2: buildDefaults.WebView2,
- Dockerfile: buildDefaults.Dockerfile,
- Registry: buildDefaults.Registry,
- Image: buildDefaults.Image,
- Tags: append([]string{}, buildDefaults.Tags...),
- BuildArgs: CloneStringMap(buildDefaults.BuildArgs),
- Push: buildDefaults.Push || push,
- Load: buildDefaults.Load,
- LinuxKitConfig: buildDefaults.LinuxKitConfig,
- Formats: append([]string{}, buildDefaults.Formats...),
- LinuxKit: cloneLinuxKitConfig(buildConfig.LinuxKit),
- }
-
- if imageName != "" {
- cfg.Image = imageName
- }
-
- return cfg
-}
-
-func versionIsSafeRelease(version string) bool {
- return ValidateVersionString(version).OK
-}
-
-func stripVersionTemplateFlags(values []string) []string {
- if len(values) == 0 {
- return values
- }
-
- filtered := make([]string, 0, len(values))
- for _, value := range values {
- if containsVersionTemplate(value) {
- continue
- }
- filtered = append(filtered, value)
- }
-
- return filtered
-}
-
-func stripVersionTemplateValues(values []string) []string {
- if len(values) == 0 {
- return values
- }
-
- filtered := make([]string, 0, len(values))
- for _, value := range values {
- if containsVersionTemplate(value) {
- continue
- }
- filtered = append(filtered, value)
- }
-
- return filtered
-}
-
-func containsVersionTemplate(value string) bool {
- return core.Contains(value, "v{{.Version}}") ||
- core.Contains(value, "v{{Version}}") ||
- core.Contains(value, "{{.Tag}}") ||
- core.Contains(value, "{{Tag}}") ||
- core.Contains(value, "{{.Version}}") ||
- core.Contains(value, "{{Version}}")
-}
diff --git a/pkg/build/runtime_config_example_test.go b/pkg/build/runtime_config_example_test.go
deleted file mode 100644
index a9ea3d2..0000000
--- a/pkg/build/runtime_config_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleRuntimeConfigFromBuildConfig references RuntimeConfigFromBuildConfig on this package API surface.
-func ExampleRuntimeConfigFromBuildConfig() {
- _ = RuntimeConfigFromBuildConfig
- core.Println("RuntimeConfigFromBuildConfig")
- // Output: RuntimeConfigFromBuildConfig
-}
diff --git a/pkg/build/runtime_config_test.go b/pkg/build/runtime_config_test.go
deleted file mode 100644
index b4fcbe9..0000000
--- a/pkg/build/runtime_config_test.go
+++ /dev/null
@@ -1,274 +0,0 @@
-package build
-
-import (
- "testing"
-
- core "dappco.re/go"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func TestBuild_RuntimeConfigFromBuildConfig_Good(t *testing.T) {
- source := &BuildConfig{
- Project: Project{
- Name: "Core",
- Main: "./cmd/core",
- Binary: "core",
- },
- Build: Build{
- CGO: true,
- Obfuscate: true,
- DenoBuild: "deno task bundle",
- NSIS: true,
- WebView2: "embed",
- Flags: []string{"-mod=readonly"},
- LDFlags: []string{"-s", "-w"},
- BuildTags: []string{"integration"},
- Env: []string{"FOO=bar"},
- Cache: CacheConfig{Enabled: true, Paths: []string{"/tmp/go-build"}},
- Dockerfile: "build/Dockerfile",
- Registry: "ghcr.io",
- Image: "host-uk/core",
- Tags: []string{"latest"},
- BuildArgs: map[string]string{"VERSION": "1.2.3"},
- Push: false,
- Load: true,
- LinuxKitConfig: ".core/linuxkit/core.yaml",
- Formats: []string{"iso", "qcow2"},
- },
- LinuxKit: LinuxKitConfig{
- Base: "core-dev",
- Packages: []string{"git"},
- Mounts: []string{"/workspace"},
- GPU: true,
- Formats: []string{"oci", "apple"},
- Registry: "ghcr.io/dappcore",
- },
- }
-
- cfg := RuntimeConfigFromBuildConfig(storage.Local, "/workspace/core", "/workspace/core/dist", "core-bin", source, true, "override/image", "v1.2.3")
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual(storage.Local, cfg.FS) {
- t.Fatalf("want %v, got %v", storage.Local, cfg.FS)
- }
- if !stdlibAssertEqual(source.Project, cfg.Project) {
- t.Fatalf("want %v, got %v", source.Project, cfg.Project)
- }
- if !stdlibAssertEqual("/workspace/core", cfg.ProjectDir) {
- t.Fatalf("want %v, got %v", "/workspace/core", cfg.ProjectDir)
- }
- if !stdlibAssertEqual("/workspace/core/dist", cfg.OutputDir) {
- t.Fatalf("want %v, got %v", "/workspace/core/dist", cfg.OutputDir)
- }
- if !stdlibAssertEqual("core-bin", cfg.Name) {
- t.Fatalf("want %v, got %v", "core-bin", cfg.Name)
- }
- if !stdlibAssertEqual("v1.2.3", cfg.Version) {
- t.Fatalf("want %v, got %v", "v1.2.3", cfg.Version)
- }
- if !stdlibAssertEqual([]string{"-mod=readonly"}, cfg.Flags) {
- t.Fatalf("want %v, got %v", []string{"-mod=readonly"}, cfg.Flags)
- }
- if !stdlibAssertEqual([]string{"-s", "-w"}, cfg.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w"}, cfg.LDFlags)
- }
- if !stdlibAssertEqual([]string{"integration"}, cfg.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"integration"}, cfg.BuildTags)
- }
- if !stdlibAssertEqual([]string{"FOO=bar"}, cfg.Env) {
- t.Fatalf("want %v, got %v", []string{"FOO=bar"}, cfg.Env)
- }
- if !stdlibAssertEqual(CacheConfig{Enabled: true, Paths: []string{"/tmp/go-build"}}, cfg.Cache) {
- t.Fatalf("want %v, got %v", CacheConfig{Enabled: true, Paths: []string{"/tmp/go-build"}}, cfg.Cache)
- }
- if !(cfg.CGO) {
- t.Fatal("expected true")
- }
- if !(cfg.Obfuscate) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("deno task bundle", cfg.DenoBuild) {
- t.Fatalf("want %v, got %v", "deno task bundle", cfg.DenoBuild)
- }
- if !(cfg.NSIS) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual("embed", cfg.WebView2) {
- t.Fatalf("want %v, got %v", "embed", cfg.WebView2)
- }
- if !stdlibAssertEqual("build/Dockerfile", cfg.Dockerfile) {
- t.Fatalf("want %v, got %v", "build/Dockerfile", cfg.Dockerfile)
- }
- if !stdlibAssertEqual("ghcr.io", cfg.Registry) {
- t.Fatalf("want %v, got %v", "ghcr.io", cfg.Registry)
- }
- if !stdlibAssertEqual("override/image", cfg.Image) {
- t.Fatalf("want %v, got %v", "override/image", cfg.Image)
- }
- if !stdlibAssertEqual([]string{"latest"}, cfg.Tags) {
- t.Fatalf("want %v, got %v", []string{"latest"}, cfg.Tags)
- }
- if !stdlibAssertEqual(map[string]string{"VERSION": "1.2.3"}, cfg.BuildArgs) {
- t.Fatalf("want %v, got %v", map[string]string{"VERSION": "1.2.3"}, cfg.BuildArgs)
- }
- if !(cfg.Push) {
- t.Fatal("expected true")
- }
- if !(cfg.Load) {
- t.Fatal("expected true")
- }
- if !stdlibAssertEqual(".core/linuxkit/core.yaml", cfg.LinuxKitConfig) {
- t.Fatalf("want %v, got %v", ".core/linuxkit/core.yaml", cfg.LinuxKitConfig)
- }
- if !stdlibAssertEqual([]string{"iso", "qcow2"}, cfg.Formats) {
- t.Fatalf("want %v, got %v", []string{"iso", "qcow2"}, cfg.Formats)
- }
- if !stdlibAssertEqual(LinuxKitConfig{Base: "core-dev", Packages: []string{"git"}, Mounts: []string{"/workspace"}, GPU: true, Formats: []string{"oci", "apple"}, Registry: "ghcr.io/dappcore"}, cfg.LinuxKit) {
- t.Fatalf("want %v, got %v", LinuxKitConfig{Base: "core-dev", Packages: []string{"git"}, Mounts: []string{"/workspace"}, GPU: true, Formats: []string{"oci", "apple"}, Registry: "ghcr.io/dappcore"}, cfg.LinuxKit)
- }
-
- cfg.Flags[0] = "-trimpath"
- cfg.LDFlags[0] = "-X"
- cfg.BuildTags[0] = "ui"
- cfg.Env[0] = "BAR=baz"
- cfg.Tags[0] = "stable"
- cfg.BuildArgs["VERSION"] = "2.0.0"
- cfg.LinuxKit.Packages[0] = "task"
- if !stdlibAssertEqual([]string{"-mod=readonly"}, source.Build.Flags) {
- t.Fatalf("want %v, got %v", []string{"-mod=readonly"}, source.Build.Flags)
- }
- if !stdlibAssertEqual([]string{"-s", "-w"}, source.Build.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w"}, source.Build.LDFlags)
- }
- if !stdlibAssertEqual([]string{"integration"}, source.Build.BuildTags) {
- t.Fatalf("want %v, got %v", []string{"integration"}, source.Build.BuildTags)
- }
- if !stdlibAssertEqual([]string{"FOO=bar"}, source.Build.Env) {
- t.Fatalf("want %v, got %v", []string{"FOO=bar"}, source.Build.Env)
- }
- if !stdlibAssertEqual([]string{"latest"}, source.Build.Tags) {
- t.Fatalf("want %v, got %v", []string{"latest"}, source.Build.Tags)
- }
- if !stdlibAssertEqual(map[string]string{"VERSION": "1.2.3"}, source.Build.BuildArgs) {
- t.Fatalf("want %v, got %v", map[string]string{"VERSION": "1.2.3"}, source.Build.BuildArgs)
- }
- if !stdlibAssertEqual([]string{"git"}, source.LinuxKit.Packages) {
- t.Fatalf("want %v, got %v", []string{"git"}, source.LinuxKit.Packages)
- }
-
-}
-
-func TestBuild_RuntimeConfigFromBuildConfig_ExpandsVersionTemplates_Good(t *testing.T) {
- source := &BuildConfig{
- Build: Build{
- Flags: []string{"-X-build=v{{.Version}}"},
- LDFlags: []string{"-X main.Version={{.Tag}}"},
- Env: []string{"RELEASE_TAG={{.Tag}}", "IMAGE_TAG=v{{.Version}}"},
- },
- }
-
- cfg := RuntimeConfigFromBuildConfig(storage.Local, "/workspace/core", "/workspace/core/dist", "core-bin", source, false, "", "v1.2.3")
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual([]string{"-X-build=v1.2.3"}, cfg.Flags) {
- t.Fatalf("want %v, got %v", []string{"-X-build=v1.2.3"}, cfg.Flags)
- }
- if !stdlibAssertEqual([]string{"-X main.Version=v1.2.3"}, cfg.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-X main.Version=v1.2.3"}, cfg.LDFlags)
- }
- if !stdlibAssertEqual([]string{"RELEASE_TAG=v1.2.3", "IMAGE_TAG=v1.2.3"}, cfg.Env) {
- t.Fatalf("want %v, got %v", []string{"RELEASE_TAG=v1.2.3", "IMAGE_TAG=v1.2.3"}, cfg.Env)
- }
- if !stdlibAssertEqual([]string{"-X-build=v{{.Version}}"}, source.Build.Flags) {
- t.Fatalf("want %v, got %v", []string{"-X-build=v{{.Version}}"}, source.Build.Flags)
- }
- if !stdlibAssertEqual([]string{"-X main.Version={{.Tag}}"}, source.Build.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-X main.Version={{.Tag}}"}, source.Build.LDFlags)
- }
- if !stdlibAssertEqual([]string{"RELEASE_TAG={{.Tag}}", "IMAGE_TAG=v{{.Version}}"}, source.Build.Env) {
- t.Fatalf("want %v, got %v", []string{"RELEASE_TAG={{.Tag}}", "IMAGE_TAG=v{{.Version}}"}, source.Build.Env)
- }
-
-}
-
-func TestBuild_RuntimeConfigFromBuildConfig_StripsUnsafeVersionTemplateFlags(t *testing.T) {
- source := &BuildConfig{
- Build: Build{
- LDFlags: []string{
- "-s",
- "-w",
- "-X main.Version={{.Tag}}",
- "-X build.commit=abc123",
- },
- },
- }
-
- cfg := RuntimeConfigFromBuildConfig(storage.Local, "/workspace/core", "/workspace/core/dist", "core-bin", source, false, "", "v1.2.3 -bad")
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual([]string{"-s", "-w", "-X build.commit=abc123"}, cfg.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w", "-X build.commit=abc123"}, cfg.LDFlags)
- }
- if !stdlibAssertEmpty(cfg.Flags) {
- t.Fatalf("expected empty, got %v", cfg.Flags)
- }
- if !stdlibAssertEmpty(cfg.Env) {
- t.Fatalf("expected empty, got %v", cfg.Env)
- }
- if !stdlibAssertEqual([]string{"-s", "-w", "-X main.Version={{.Tag}}", "-X build.commit=abc123"}, source.Build.LDFlags) {
- t.Fatalf("want %v, got %v", []string{"-s", "-w", "-X main.Version={{.Tag}}", "-X build.commit=abc123"}, source.Build.LDFlags)
- }
-
-}
-
-func TestBuild_RuntimeConfigFromBuildConfig_UsesRFCPreBuildAliases_Good(t *testing.T) {
- source := &BuildConfig{
- PreBuild: PreBuild{
- Deno: "deno task build",
- Npm: "npm run build",
- },
- }
-
- cfg := RuntimeConfigFromBuildConfig(storage.Local, "/workspace/core", "/workspace/core/dist", "core-bin", source, false, "", "v1.2.3")
- if stdlibAssertNil(cfg) {
- t.Fatal("expected non-nil")
- }
- if !stdlibAssertEqual("deno task build", cfg.DenoBuild) {
- t.Fatalf("want %v, got %v", "deno task build", cfg.DenoBuild)
- }
- if !stdlibAssertEqual("npm run build", cfg.NpmBuild) {
- t.Fatalf("want %v, got %v", "npm run build", cfg.NpmBuild)
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestRuntimeConfig_RuntimeConfigFromBuildConfig_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = RuntimeConfigFromBuildConfig(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), &BuildConfig{}, true, "agent", "v1.2.3")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestRuntimeConfig_RuntimeConfigFromBuildConfig_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = RuntimeConfigFromBuildConfig(storage.NewMemoryMedium(), "", "", "", nil, false, "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestRuntimeConfig_RuntimeConfigFromBuildConfig_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = RuntimeConfigFromBuildConfig(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), &BuildConfig{}, true, "agent", "v1.2.3")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/setup.go b/pkg/build/setup.go
deleted file mode 100644
index e43f2da..0000000
--- a/pkg/build/setup.go
+++ /dev/null
@@ -1,302 +0,0 @@
-package build
-
-import (
- "sort"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// SetupTool identifies a toolchain or installer surface required by the
-// action-style setup phase.
-type SetupTool string
-
-const (
- // SetupToolGo installs the Go toolchain.
- SetupToolGo SetupTool = "go"
- // SetupToolGarble installs garble for obfuscated Go and Wails builds.
- SetupToolGarble SetupTool = "garble"
- // SetupToolTask installs the Task CLI for Taskfile-driven builds.
- SetupToolTask SetupTool = "task"
- // SetupToolNode installs Node.js/Corepack for frontend-backed builds.
- SetupToolNode SetupTool = "node"
- // SetupToolWails installs the Wails CLI for Wails-backed builds.
- SetupToolWails SetupTool = "wails"
- // SetupToolPython installs Python for Conan and MkDocs flows.
- SetupToolPython SetupTool = "python"
- // SetupToolPHP installs PHP for Composer-backed builds.
- SetupToolPHP SetupTool = "php"
- // SetupToolComposer installs Composer for PHP builds.
- SetupToolComposer SetupTool = "composer"
- // SetupToolRust installs Rust/Cargo for Rust builds.
- SetupToolRust SetupTool = "rust"
- // SetupToolConan installs Conan for C++ builds.
- SetupToolConan SetupTool = "conan"
- // SetupToolMkDocs installs MkDocs for docs builds.
- SetupToolMkDocs SetupTool = "mkdocs"
- // SetupToolDeno installs Deno for manifest-backed or override-driven builds.
- SetupToolDeno SetupTool = "deno"
-)
-
-// SetupStep describes one toolchain requirement in the setup plan.
-type SetupStep struct {
- Tool SetupTool `json:"tool"`
- Reason string `json:"reason"`
-}
-
-// SetupPlan is the Go-side equivalent of the action setup orchestration.
-// It is pure data: discovery + config in, setup requirements out.
-type SetupPlan struct {
- ProjectDir string
- PrimaryStack string
- PrimaryStackSuggestion string
- FrontendDirs []string
- LinuxPackages []string
- Steps []SetupStep
-}
-
-// ComputeSetupPlan derives the action-style setup requirements from discovery
-// and config. When discovery is nil the function performs a fresh DiscoverFull
-// pass using the provided filesystem and directory.
-func ComputeSetupPlan(fs storage.Medium, dir string, cfg *BuildConfig, discovery *DiscoveryResult) core.Result {
- if fs == nil {
- fs = storage.Local
- }
-
- if discovery == nil {
- discovered := DiscoverFull(fs, dir)
- if !discovered.OK {
- return discovered
- }
- discovery = discovered.Value.(*DiscoveryResult)
- }
-
- configuredType := resolveConfiguredBuildType(cfg, discovery)
- denoRequested := DenoRequested(configuredDenoBuild(cfg))
- hasTaskfile := configuredType == string(ProjectTypeTaskfile) || discovery.HasTaskfile || containsProjectType(discovery.Types, ProjectTypeTaskfile)
- hasWails := configuredType == string(ProjectTypeWails) || discovery.PrimaryStackSuggestion == "wails2"
- hasCPP := configuredType == string(ProjectTypeCPP) || containsProjectType(discovery.Types, ProjectTypeCPP) || discovery.HasRootCMakeLists
- hasDocs := configuredType == string(ProjectTypeDocs) || containsProjectType(discovery.Types, ProjectTypeDocs) || discovery.HasDocsConfig
- hasPython := configuredType == string(ProjectTypePython) || containsProjectType(discovery.Types, ProjectTypePython)
- hasPHP := configuredType == string(ProjectTypePHP) || containsProjectType(discovery.Types, ProjectTypePHP) || discovery.HasRootComposerJSON
- hasRust := configuredType == string(ProjectTypeRust) || containsProjectType(discovery.Types, ProjectTypeRust) || discovery.HasRootCargoToml
- hasNode := configuredType == string(ProjectTypeNode) || hasWails || discovery.HasPackageJSON
- hasGo := configuredType == string(ProjectTypeGo) || hasWails || hasTaskfile || discovery.HasGoToolchain || containsProjectType(discovery.Types, ProjectTypeGo)
-
- primaryStack := discovery.PrimaryStack
- primaryStackSuggestion := discovery.PrimaryStackSuggestion
- if configuredType != "" {
- primaryStack = configuredType
- primaryStackSuggestion = SuggestStack([]ProjectType{ProjectType(configuredType)})
- }
- linuxPackages := resolveSetupLinuxPackages(fs, configuredType, discovery, hasWails)
-
- plan := &SetupPlan{
- ProjectDir: dir,
- PrimaryStack: primaryStack,
- PrimaryStackSuggestion: primaryStackSuggestion,
- FrontendDirs: ResolveFrontendSetupDirs(fs, dir, denoRequested),
- LinuxPackages: linuxPackages,
- }
-
- if hasGo {
- plan.addStep(SetupToolGo, "Go-backed build stack detected")
- }
- if cfg != nil && cfg.Build.Obfuscate {
- plan.addStep(SetupToolGarble, "build.obfuscate is enabled")
- }
- if hasTaskfile {
- plan.addStep(SetupToolTask, "Taskfile project detected")
- }
- if hasNode {
- plan.addStep(SetupToolNode, "frontend package manifests detected")
- }
- if hasWails {
- plan.addStep(SetupToolWails, "Wails stack detected")
- }
- if hasPython || hasCPP || hasDocs {
- plan.addStep(SetupToolPython, pythonSetupReason(hasPython, hasCPP, hasDocs))
- }
- if hasPHP {
- plan.addStep(SetupToolPHP, "composer.json detected")
- plan.addStep(SetupToolComposer, "composer-backed build detected")
- }
- if hasRust {
- plan.addStep(SetupToolRust, "Cargo.toml detected")
- }
- if hasCPP {
- plan.addStep(SetupToolConan, "C++ stack detected")
- }
- if hasDocs {
- plan.addStep(SetupToolMkDocs, "MkDocs config detected")
- }
- if discovery.HasDenoManifest || denoRequested {
- plan.addStep(SetupToolDeno, "Deno manifest or override detected")
- }
-
- return core.Ok(plan)
-}
-
-func pythonSetupReason(hasPython, hasCPP, hasDocs bool) string {
- switch {
- case hasPython:
- return "Python project detected"
- case hasCPP && hasDocs:
- return "docs and C++ setup relies on Python tooling"
- case hasCPP:
- return "C++ setup relies on Python tooling"
- case hasDocs:
- return "MkDocs setup relies on Python tooling"
- default:
- return "Python tooling required"
- }
-}
-
-// ResolveFrontendSetupDirs returns frontend directories that participate in the
-// action-style setup phase.
-//
-// dirs := build.ResolveFrontendSetupDirs(storage.Local, ".", true)
-// // ["./frontend"] when the project only has an empty frontend/ directory
-// // ["./apps/web"] when a nested package.json is detected
-func ResolveFrontendSetupDirs(fs storage.Medium, dir string, allowEmptyFallback bool) []string {
- if fs == nil {
- fs = storage.Local
- }
-
- var dirs []string
-
- rootHasManifest := hasFrontendManifest(fs, dir)
- frontendDir := ax.Join(dir, "frontend")
- frontendHasManifest := fs.IsDir(frontendDir) && hasFrontendManifest(fs, frontendDir)
-
- if rootHasManifest {
- dirs = append(dirs, dir)
- }
- if frontendHasManifest {
- dirs = append(dirs, frontendDir)
- }
-
- collectFrontendSetupDirs(fs, dir, 0, &dirs)
-
- if len(dirs) == 0 && allowEmptyFallback {
- if fs.IsDir(frontendDir) {
- dirs = append(dirs, frontendDir)
- } else {
- dirs = append(dirs, dir)
- }
- }
-
- return deduplicateAndSortPaths(dirs)
-}
-
-func collectFrontendSetupDirs(fs storage.Medium, dir string, depth int, dirs *[]string) {
- if depth >= 2 {
- return
- }
-
- entriesResult := fs.List(dir)
- if !entriesResult.OK {
- return
- }
-
- for _, entry := range entriesResult.Value.([]core.FsDirEntry) {
- if !entry.IsDir() {
- continue
- }
-
- name := entry.Name()
- if shouldSkipSubtreeDir(name) || name == "frontend" {
- continue
- }
-
- candidateDir := ax.Join(dir, name)
- if hasFrontendManifest(fs, candidateDir) {
- *dirs = append(*dirs, candidateDir)
- }
-
- collectFrontendSetupDirs(fs, candidateDir, depth+1, dirs)
- }
-}
-
-func deduplicateAndSortPaths(paths []string) []string {
- if len(paths) == 0 {
- return nil
- }
-
- seen := make(map[string]struct{}, len(paths))
- result := make([]string, 0, len(paths))
-
- for _, path := range paths {
- path = ax.Clean(path)
- if path == "" {
- continue
- }
- if _, ok := seen[path]; ok {
- continue
- }
- seen[path] = struct{}{}
- result = append(result, path)
- }
-
- sort.Strings(result)
- return result
-}
-
-func configuredDenoBuild(cfg *BuildConfig) string {
- if cfg == nil {
- return ""
- }
- return core.Trim(cfg.Build.DenoBuild)
-}
-
-func resolveConfiguredBuildType(cfg *BuildConfig, discovery *DiscoveryResult) string {
- if cfg != nil {
- if value := core.Lower(core.Trim(cfg.Build.Type)); value != "" {
- return value
- }
- }
- if discovery != nil {
- return core.Lower(core.Trim(discovery.ConfiguredType))
- }
- return ""
-}
-
-func resolveSetupLinuxPackages(fs storage.Medium, configuredType string, discovery *DiscoveryResult, hasWails bool) []string {
- if discovery == nil {
- return nil
- }
-
- packages := deduplicateStrings(append([]string{}, discovery.LinuxPackages...))
- if len(packages) > 0 {
- return packages
- }
-
- if !hasWails && configuredType != string(ProjectTypeWails) {
- return nil
- }
-
- distro := core.Trim(discovery.Distro)
- if distro == "" {
- distro = detectDistroVersion(fs)
- }
-
- return ResolveLinuxPackages([]ProjectType{ProjectTypeWails}, distro)
-}
-
-func (p *SetupPlan) addStep(tool SetupTool, reason string) {
- if p == nil {
- return
- }
-
- for _, step := range p.Steps {
- if step.Tool == tool {
- return
- }
- }
-
- p.Steps = append(p.Steps, SetupStep{
- Tool: tool,
- Reason: reason,
- })
-}
diff --git a/pkg/build/setup_example_test.go b/pkg/build/setup_example_test.go
deleted file mode 100644
index 200014b..0000000
--- a/pkg/build/setup_example_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleComputeSetupPlan references ComputeSetupPlan on this package API surface.
-func ExampleComputeSetupPlan() {
- _ = ComputeSetupPlan
- core.Println("ComputeSetupPlan")
- // Output: ComputeSetupPlan
-}
-
-// ExampleResolveFrontendSetupDirs references ResolveFrontendSetupDirs on this package API surface.
-func ExampleResolveFrontendSetupDirs() {
- _ = ResolveFrontendSetupDirs
- core.Println("ResolveFrontendSetupDirs")
- // Output: ResolveFrontendSetupDirs
-}
diff --git a/pkg/build/setup_test.go b/pkg/build/setup_test.go
deleted file mode 100644
index 7703181..0000000
--- a/pkg/build/setup_test.go
+++ /dev/null
@@ -1,266 +0,0 @@
-package build
-
-import (
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func requireSetupOKResult(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requireSetupPlan(t *testing.T, result core.Result) *SetupPlan {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(*SetupPlan)
-}
-
-func TestSetup_ComputeSetupPlan_Good(t *testing.T) {
- t.Run("wails monorepo adds Go Node Wails Garble and Linux packages", func(t *testing.T) {
- dir := t.TempDir()
- nestedFrontend := ax.Join(dir, "apps", "web")
- requireSetupOKResult(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module example.com/app\n"), 0o644))
- requireSetupOKResult(t, ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644))
- requireSetupOKResult(t, ax.MkdirAll(nestedFrontend, 0o755))
- requireSetupOKResult(t, ax.WriteFile(ax.Join(nestedFrontend, "package.json"), []byte("{}"), 0o644))
-
- cfg := DefaultConfig()
- cfg.Build.Obfuscate = true
-
- discovery := &DiscoveryResult{
- Types: []ProjectType{ProjectTypeWails, ProjectTypeGo, ProjectTypeNode},
- PrimaryStack: "wails",
- PrimaryStackSuggestion: "wails2",
- HasGoToolchain: true,
- HasPackageJSON: true,
- LinuxPackages: []string{"libwebkit2gtk-4.1-dev"},
- }
-
- plan := requireSetupPlan(t, ComputeSetupPlan(storage.Local, dir, cfg, discovery))
- if !stdlibAssertEqual([]SetupTool{SetupToolGo, SetupToolGarble, SetupToolNode, SetupToolWails}, setupTools(plan)) {
- t.Fatalf("want %v, got %v", []SetupTool{SetupToolGo, SetupToolGarble, SetupToolNode, SetupToolWails}, setupTools(plan))
- }
- if !stdlibAssertEqual([]string{nestedFrontend}, plan.FrontendDirs) {
- t.Fatalf("want %v, got %v", []string{nestedFrontend}, plan.FrontendDirs)
- }
- if !stdlibAssertEqual([]string{"libwebkit2gtk-4.1-dev"}, plan.LinuxPackages) {
- t.Fatalf("want %v, got %v", []string{"libwebkit2gtk-4.1-dev"}, plan.LinuxPackages)
- }
-
- })
-
- t.Run("docs plus package json keeps Node and adds Python plus MkDocs", func(t *testing.T) {
- dir := t.TempDir()
- requireSetupOKResult(t, ax.WriteFile(ax.Join(dir, "mkdocs.yml"), []byte("site_name: Demo\n"), 0o644))
- requireSetupOKResult(t, ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644))
-
- discovery := &DiscoveryResult{
- Types: []ProjectType{ProjectTypeNode, ProjectTypeDocs},
- PrimaryStack: "node",
- PrimaryStackSuggestion: "node",
- HasDocsConfig: true,
- HasPackageJSON: true,
- }
-
- plan := requireSetupPlan(t, ComputeSetupPlan(storage.Local, dir, DefaultConfig(), discovery))
- if !stdlibAssertEqual([]SetupTool{SetupToolNode, SetupToolPython, SetupToolMkDocs}, setupTools(plan)) {
- t.Fatalf("want %v, got %v", []SetupTool{SetupToolNode, SetupToolPython, SetupToolMkDocs}, setupTools(plan))
- }
- if !stdlibAssertEqual([]string{dir}, plan.FrontendDirs) {
- t.Fatalf("want %v, got %v", []string{dir}, plan.FrontendDirs)
- }
-
- })
-
- t.Run("cpp stack adds Python and Conan", func(t *testing.T) {
- dir := t.TempDir()
- requireSetupOKResult(t, ax.WriteFile(ax.Join(dir, "CMakeLists.txt"), []byte("cmake_minimum_required(VERSION 3.20)\n"), 0o644))
-
- discovery := &DiscoveryResult{
- Types: []ProjectType{ProjectTypeCPP},
- PrimaryStack: "cpp",
- PrimaryStackSuggestion: "cpp",
- HasRootCMakeLists: true,
- }
-
- plan := requireSetupPlan(t, ComputeSetupPlan(storage.Local, dir, DefaultConfig(), discovery))
- if !stdlibAssertEqual([]SetupTool{SetupToolPython, SetupToolConan}, setupTools(plan)) {
- t.Fatalf("want %v, got %v", []SetupTool{SetupToolPython, SetupToolConan}, setupTools(plan))
- }
- if !stdlibAssertEmpty(plan.FrontendDirs) {
- t.Fatalf("expected empty, got %v", plan.FrontendDirs)
- }
-
- })
-
- t.Run("python stack adds Python tooling", func(t *testing.T) {
- dir := t.TempDir()
- requireSetupOKResult(t, ax.WriteFile(ax.Join(dir, "pyproject.toml"), []byte("[project]\nname='demo'\n"), 0o644))
-
- discovery := &DiscoveryResult{
- Types: []ProjectType{ProjectTypePython},
- PrimaryStack: "python",
- PrimaryStackSuggestion: "python",
- }
-
- plan := requireSetupPlan(t, ComputeSetupPlan(storage.Local, dir, DefaultConfig(), discovery))
- if !stdlibAssertEqual([]SetupTool{SetupToolPython}, setupTools(plan)) {
- t.Fatalf("want %v, got %v", []SetupTool{SetupToolPython}, setupTools(plan))
- }
-
- })
-
- t.Run("taskfile stack adds Go and Task even without go markers", func(t *testing.T) {
- dir := t.TempDir()
- requireSetupOKResult(t, ax.WriteFile(ax.Join(dir, "Taskfile.yaml"), []byte("version: '3'\n"), 0o644))
-
- discovery := &DiscoveryResult{
- Types: []ProjectType{ProjectTypeTaskfile},
- PrimaryStack: "taskfile",
- PrimaryStackSuggestion: "taskfile",
- }
-
- plan := requireSetupPlan(t, ComputeSetupPlan(storage.Local, dir, DefaultConfig(), discovery))
- if !stdlibAssertEqual([]SetupTool{SetupToolGo, SetupToolTask}, setupTools(plan)) {
- t.Fatalf("want %v, got %v", []SetupTool{SetupToolGo, SetupToolTask}, setupTools(plan))
- }
-
- })
-
- t.Run("configured wails stack adds Go Node and Wails without frontend markers", func(t *testing.T) {
- dir := t.TempDir()
-
- cfg := DefaultConfig()
- cfg.Build.Type = "wails"
-
- plan := requireSetupPlan(t, ComputeSetupPlan(storage.Local, dir, cfg, &DiscoveryResult{}))
- if !stdlibAssertEqual([]SetupTool{SetupToolGo, SetupToolNode, SetupToolWails}, setupTools(plan)) {
- t.Fatalf("want %v, got %v", []SetupTool{SetupToolGo, SetupToolNode, SetupToolWails}, setupTools(plan))
- }
- if !stdlibAssertEqual("wails", plan.PrimaryStack) {
- t.Fatalf("want %v, got %v", "wails", plan.PrimaryStack)
- }
- if !stdlibAssertEqual("wails2", plan.PrimaryStackSuggestion) {
- t.Fatalf("want %v, got %v", "wails2", plan.PrimaryStackSuggestion)
- }
-
- })
-
- t.Run("configured wails stack derives Linux packages from distro when discovery is partial", func(t *testing.T) {
- dir := t.TempDir()
-
- cfg := DefaultConfig()
- cfg.Build.Type = "wails"
-
- plan := requireSetupPlan(t, ComputeSetupPlan(storage.Local, dir, cfg, &DiscoveryResult{
- Distro: "24.04",
- }))
- if !stdlibAssertEqual([]string{"libwebkit2gtk-4.1-dev"}, plan.LinuxPackages) {
- t.Fatalf("want %v, got %v", []string{"libwebkit2gtk-4.1-dev"}, plan.LinuxPackages)
- }
-
- })
-
- t.Run("deno override enables Deno and fallback frontend dir", func(t *testing.T) {
- dir := t.TempDir()
-
- cfg := DefaultConfig()
- cfg.Build.DenoBuild = "deno task bundle"
-
- plan := requireSetupPlan(t, ComputeSetupPlan(storage.Local, dir, cfg, &DiscoveryResult{}))
- if !stdlibAssertEqual([]SetupTool{SetupToolDeno}, setupTools(plan)) {
- t.Fatalf("want %v, got %v", []SetupTool{SetupToolDeno}, setupTools(plan))
- }
- if !stdlibAssertEqual([]string{dir}, plan.FrontendDirs) {
- t.Fatalf("want %v, got %v", []string{dir}, plan.FrontendDirs)
- }
-
- })
-}
-
-func TestSetup_ResolveFrontendSetupDirs_Good(t *testing.T) {
- t.Run("returns root frontend and nested manifests in deterministic order", func(t *testing.T) {
- dir := t.TempDir()
- frontendDir := ax.Join(dir, "frontend")
- nestedA := ax.Join(dir, "apps", "alpha")
- nestedB := ax.Join(dir, "apps", "beta")
- requireSetupOKResult(t, ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644))
- requireSetupOKResult(t, ax.MkdirAll(frontendDir, 0o755))
- requireSetupOKResult(t, ax.WriteFile(ax.Join(frontendDir, "package.json"), []byte("{}"), 0o644))
- requireSetupOKResult(t, ax.MkdirAll(nestedB, 0o755))
- requireSetupOKResult(t, ax.WriteFile(ax.Join(nestedB, "deno.json"), []byte("{}"), 0o644))
- requireSetupOKResult(t, ax.MkdirAll(nestedA, 0o755))
- requireSetupOKResult(t, ax.WriteFile(ax.Join(nestedA, "package.json"), []byte("{}"), 0o644))
- if !stdlibAssertEqual([]string{dir, nestedA, nestedB, frontendDir}, ResolveFrontendSetupDirs(storage.Local, dir, false)) {
- t.Fatalf("want %v, got %v", []string{dir, nestedA, nestedB, frontendDir}, ResolveFrontendSetupDirs(storage.Local, dir, false))
- }
-
- })
-
- t.Run("uses frontend fallback when deno is requested without manifests", func(t *testing.T) {
- dir := t.TempDir()
- frontendDir := ax.Join(dir, "frontend")
- requireSetupOKResult(t, ax.MkdirAll(frontendDir, 0o755))
- if !stdlibAssertEqual([]string{frontendDir}, ResolveFrontendSetupDirs(storage.Local, dir, true)) {
- t.Fatalf("want %v, got %v", []string{frontendDir}, ResolveFrontendSetupDirs(storage.Local, dir, true))
- }
-
- })
-}
-
-func setupTools(plan *SetupPlan) []SetupTool {
- if plan == nil {
- return nil
- }
-
- tools := make([]SetupTool, 0, len(plan.Steps))
- for _, step := range plan.Steps {
- tools = append(tools, step.Tool)
- }
- return tools
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestSetup_ComputeSetupPlan_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ComputeSetupPlan(storage.NewMemoryMedium(), "", nil, nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestSetup_ComputeSetupPlan_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ComputeSetupPlan(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), &BuildConfig{}, &DiscoveryResult{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestSetup_ResolveFrontendSetupDirs_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveFrontendSetupDirs(storage.NewMemoryMedium(), "", false)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestSetup_ResolveFrontendSetupDirs_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveFrontendSetupDirs(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), true)
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/signing/codesign.go b/pkg/build/signing/codesign.go
deleted file mode 100644
index 5558fbd..0000000
--- a/pkg/build/signing/codesign.go
+++ /dev/null
@@ -1,182 +0,0 @@
-package signing
-
-import (
- "context"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// MacOSSigner signs binaries using macOS codesign.
-//
-// s := signing.NewMacOSSigner(cfg.MacOS)
-type MacOSSigner struct {
- config MacOSConfig
-}
-
-// Compile-time interface check.
-var _ Signer = (*MacOSSigner)(nil)
-
-// NewMacOSSigner creates a new macOS signer.
-//
-// s := signing.NewMacOSSigner(cfg.MacOS)
-func NewMacOSSigner(cfg MacOSConfig) *MacOSSigner {
- return &MacOSSigner{config: cfg}
-}
-
-// Name returns "codesign".
-//
-// name := s.Name() // → "codesign"
-func (s *MacOSSigner) Name() string {
- return "codesign"
-}
-
-// Available checks if running on macOS with codesign and identity configured.
-//
-// ok := s.Available() // → true if on macOS with identity set
-func (s *MacOSSigner) Available() bool {
- if core.Env("GOOS") != "darwin" {
- return false
- }
- if s.config.Identity == "" {
- return false
- }
- return resolveCodesignCli().OK
-}
-
-// Sign codesigns a binary with hardened runtime.
-//
-// err := s.Sign(ctx, storage.Local, "dist/myapp")
-func (s *MacOSSigner) Sign(ctx context.Context, fs storage.Medium, binary string) core.Result {
- if !s.Available() {
- if core.Env("GOOS") != "darwin" {
- return core.Fail(core.E("codesign.Sign", "codesign is only available on macOS", nil))
- }
- if s.config.Identity == "" {
- return core.Fail(core.E("codesign.Sign", "codesign identity not configured", nil))
- }
- return core.Fail(core.E("codesign.Sign", "codesign tool not found in PATH", nil))
- }
-
- codesignCommand := resolveCodesignCli()
- if !codesignCommand.OK {
- return core.Fail(core.E("codesign.Sign", "codesign tool not found in PATH", core.NewError(codesignCommand.Error())))
- }
-
- output := ax.CombinedOutput(ctx, "", nil, codesignCommand.Value.(string),
- "--sign", s.config.Identity,
- "--timestamp",
- "--options", `runtime`, // Hardened runtime for notarization
- "--force",
- binary,
- )
- if !output.OK {
- return core.Fail(core.E("codesign.Sign", output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// Notarize submits binary to Apple for notarization and staples the ticket.
-// This blocks until Apple responds (typically 1-5 minutes).
-//
-// err := s.Notarize(ctx, storage.Local, "dist/myapp")
-func (s *MacOSSigner) Notarize(ctx context.Context, fs storage.Medium, binary string) core.Result {
- if s.config.AppleID == "" || s.config.TeamID == "" || s.config.AppPassword == "" {
- return core.Fail(core.E("codesign.Notarize", "missing Apple credentials (apple_id, team_id, app_password)", nil))
- }
-
- zipCommand := resolveZipCli()
- if !zipCommand.OK {
- return core.Fail(core.E("codesign.Notarize", "zip tool not found in PATH", core.NewError(zipCommand.Error())))
- }
-
- xcrunCommand := resolveXcrunCli()
- if !xcrunCommand.OK {
- return core.Fail(core.E("codesign.Notarize", "xcrun tool not found in PATH", core.NewError(xcrunCommand.Error())))
- }
-
- // Create ZIP for submission
- zipPath := binary + ".zip"
- if output := ax.CombinedOutput(ctx, "", nil, zipCommand.Value.(string), "-j", zipPath, binary); !output.OK {
- return core.Fail(core.E("codesign.Notarize", "failed to create zip: "+output.Error(), core.NewError(output.Error())))
- }
- defer func() { _ = fs.Delete(zipPath) }()
-
- // Submit to Apple and wait
- if output := ax.CombinedOutput(ctx, "", nil, xcrunCommand.Value.(string), "notarytool", "submit",
- zipPath,
- "--apple-id", s.config.AppleID,
- "--team-id", s.config.TeamID,
- "--password", s.config.AppPassword,
- "--wait",
- ); !output.OK {
- return core.Fail(core.E("codesign.Notarize", "notarization failed: "+output.Error(), core.NewError(output.Error())))
- }
-
- // Staple the ticket
- if output := ax.CombinedOutput(ctx, "", nil, xcrunCommand.Value.(string), "stapler", "staple", binary); !output.OK {
- return core.Fail(core.E("codesign.Notarize", "failed to staple: "+output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// ShouldNotarize returns true if notarization is enabled.
-//
-// if s.ShouldNotarize() { ... }
-func (s *MacOSSigner) ShouldNotarize() bool {
- return s.config.Notarize
-}
-
-func resolveCodesignCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/bin/codesign",
- "/usr/local/bin/codesign",
- "/opt/homebrew/bin/codesign",
- }
- }
-
- command := ax.ResolveCommand("codesign", paths...)
- if !command.OK {
- return core.Fail(core.E("codesign.resolveCodesignCli", "codesign tool not found. Install Xcode Command Line Tools on macOS.", core.NewError(command.Error())))
- }
-
- return command
-}
-
-func resolveZipCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/bin/zip",
- "/usr/local/bin/zip",
- "/opt/homebrew/bin/zip",
- }
- }
-
- command := ax.ResolveCommand("zip", paths...)
- if !command.OK {
- return core.Fail(core.E("codesign.resolveZipCli", "zip tool not found. Install the zip utility for notarisation packaging.", core.NewError(command.Error())))
- }
-
- return command
-}
-
-func resolveXcrunCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/bin/xcrun",
- "/usr/local/bin/xcrun",
- "/opt/homebrew/bin/xcrun",
- }
- }
-
- command := ax.ResolveCommand("xcrun", paths...)
- if !command.OK {
- return core.Fail(core.E("codesign.resolveXcrunCli", "xcrun tool not found. Install Xcode Command Line Tools on macOS.", core.NewError(command.Error())))
- }
-
- return command
-}
diff --git a/pkg/build/signing/codesign_example_test.go b/pkg/build/signing/codesign_example_test.go
deleted file mode 100644
index 9e67c7a..0000000
--- a/pkg/build/signing/codesign_example_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package signing
-
-import core "dappco.re/go"
-
-// ExampleNewMacOSSigner references NewMacOSSigner on this package API surface.
-func ExampleNewMacOSSigner() {
- _ = NewMacOSSigner
- core.Println("NewMacOSSigner")
- // Output: NewMacOSSigner
-}
-
-// ExampleMacOSSigner_Name references MacOSSigner.Name on this package API surface.
-func ExampleMacOSSigner_Name() {
- _ = (*MacOSSigner).Name
- core.Println("MacOSSigner.Name")
- // Output: MacOSSigner.Name
-}
-
-// ExampleMacOSSigner_Available references MacOSSigner.Available on this package API surface.
-func ExampleMacOSSigner_Available() {
- _ = (*MacOSSigner).Available
- core.Println("MacOSSigner.Available")
- // Output: MacOSSigner.Available
-}
-
-// ExampleMacOSSigner_Sign references MacOSSigner.Sign on this package API surface.
-func ExampleMacOSSigner_Sign() {
- _ = (*MacOSSigner).Sign
- core.Println("MacOSSigner.Sign")
- // Output: MacOSSigner.Sign
-}
-
-// ExampleMacOSSigner_Notarize references MacOSSigner.Notarize on this package API surface.
-func ExampleMacOSSigner_Notarize() {
- _ = (*MacOSSigner).Notarize
- core.Println("MacOSSigner.Notarize")
- // Output: MacOSSigner.Notarize
-}
-
-// ExampleMacOSSigner_ShouldNotarize references MacOSSigner.ShouldNotarize on this package API surface.
-func ExampleMacOSSigner_ShouldNotarize() {
- _ = (*MacOSSigner).ShouldNotarize
- core.Println("MacOSSigner.ShouldNotarize")
- // Output: MacOSSigner.ShouldNotarize
-}
diff --git a/pkg/build/signing/codesign_test.go b/pkg/build/signing/codesign_test.go
deleted file mode 100644
index 1840235..0000000
--- a/pkg/build/signing/codesign_test.go
+++ /dev/null
@@ -1,376 +0,0 @@
-package signing
-
-import (
- "context"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func TestCodesign_MacOSSignerNameGood(t *testing.T) {
- s := NewMacOSSigner(MacOSConfig{Identity: "Developer ID Application: Test"})
- if !stdlibAssertEqual("codesign", s.Name()) {
- t.Fatalf("want %v, got %v", "codesign", s.Name())
- }
-
-}
-
-func TestCodesign_MacOSSignerAvailableGood(t *testing.T) {
- s := NewMacOSSigner(MacOSConfig{Identity: "Developer ID Application: Test"})
-
- if runtime.GOOS == "darwin" {
- // Just verify it doesn't panic
- _ = s.Available()
- } else {
- if s.Available() {
- t.Fatal("expected false")
- }
-
- }
-}
-
-func TestCodesign_MacOSSignerNoIdentityBad(t *testing.T) {
- s := NewMacOSSigner(MacOSConfig{})
- if s.Available() {
- t.Fatal("expected false")
- }
-
-}
-
-func TestCodesign_MacOSSignerSignBad(t *testing.T) {
- t.Run("fails when not available", func(t *testing.T) {
- if runtime.GOOS == "darwin" {
- t.Skip("skipping on macOS")
- }
- fs := storage.Local
- s := NewMacOSSigner(MacOSConfig{Identity: "test"})
- result := s.Sign(context.Background(), fs, "test")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "only available on macOS") {
- t.Fatalf("expected %v to contain %v", result.Error(), "only available on macOS")
- }
-
- })
-}
-
-func TestCodesign_MacOSSignerNotarizeBad(t *testing.T) {
- fs := storage.Local
- t.Run("fails with missing credentials", func(t *testing.T) {
- s := NewMacOSSigner(MacOSConfig{})
- result := s.Notarize(context.Background(), fs, "test")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "missing Apple credentials") {
- t.Fatalf("expected %v to contain %v", result.Error(), "missing Apple credentials")
- }
-
- })
-}
-
-func TestCodesign_MacOSSignerShouldNotarizeGood(t *testing.T) {
- s := NewMacOSSigner(MacOSConfig{Notarize: true})
- if !(s.ShouldNotarize()) {
- t.Fatal("expected true")
- }
-
- s2 := NewMacOSSigner(MacOSConfig{Notarize: false})
- if s2.ShouldNotarize() {
- t.Fatal("expected false")
- }
-
-}
-
-func TestCodesign_ResolveCodesignCliGood(t *testing.T) {
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "codesign")
- if result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- result := resolveCodesignCli(fallbackPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- command := result.Value.(string)
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestCodesign_ResolveCodesignCliBad(t *testing.T) {
- t.Setenv("PATH", "")
-
- result := resolveCodesignCli(ax.Join(t.TempDir(), "missing-codesign"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "codesign tool not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "codesign tool not found")
- }
-
-}
-
-func TestCodesign_ResolveZipCliGood(t *testing.T) {
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "zip")
- if result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- result := resolveZipCli(fallbackPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- command := result.Value.(string)
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestCodesign_ResolveZipCliBad(t *testing.T) {
- t.Setenv("PATH", "")
-
- result := resolveZipCli(ax.Join(t.TempDir(), "missing-zip"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "zip tool not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "zip tool not found")
- }
-
-}
-
-func TestCodesign_ResolveXcrunCliGood(t *testing.T) {
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "xcrun")
- if result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- result := resolveXcrunCli(fallbackPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- command := result.Value.(string)
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestCodesign_ResolveXcrunCliBad(t *testing.T) {
- t.Setenv("PATH", "")
-
- result := resolveXcrunCli(ax.Join(t.TempDir(), "missing-xcrun"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "xcrun tool not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "xcrun tool not found")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestCodesign_NewMacOSSigner_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewMacOSSigner(MacOSConfig{})
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCodesign_NewMacOSSigner_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewMacOSSigner(MacOSConfig{})
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCodesign_NewMacOSSigner_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewMacOSSigner(MacOSConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCodesign_MacOSSigner_Name_Good(t *core.T) {
- subject := &MacOSSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCodesign_MacOSSigner_Name_Bad(t *core.T) {
- subject := &MacOSSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCodesign_MacOSSigner_Name_Ugly(t *core.T) {
- subject := &MacOSSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCodesign_MacOSSigner_Available_Good(t *core.T) {
- subject := &MacOSSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Available()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCodesign_MacOSSigner_Available_Bad(t *core.T) {
- subject := &MacOSSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Available()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCodesign_MacOSSigner_Available_Ugly(t *core.T) {
- subject := &MacOSSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Available()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCodesign_MacOSSigner_Sign_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &MacOSSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Sign(ctx, storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCodesign_MacOSSigner_Sign_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &MacOSSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Sign(ctx, storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCodesign_MacOSSigner_Sign_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &MacOSSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Sign(ctx, storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCodesign_MacOSSigner_Notarize_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &MacOSSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Notarize(ctx, storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCodesign_MacOSSigner_Notarize_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &MacOSSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Notarize(ctx, storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCodesign_MacOSSigner_Notarize_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &MacOSSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Notarize(ctx, storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestCodesign_MacOSSigner_ShouldNotarize_Good(t *core.T) {
- subject := &MacOSSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ShouldNotarize()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestCodesign_MacOSSigner_ShouldNotarize_Bad(t *core.T) {
- subject := &MacOSSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ShouldNotarize()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestCodesign_MacOSSigner_ShouldNotarize_Ugly(t *core.T) {
- subject := &MacOSSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.ShouldNotarize()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/signing/gpg.go b/pkg/build/signing/gpg.go
deleted file mode 100644
index c772b7b..0000000
--- a/pkg/build/signing/gpg.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package signing
-
-import (
- "context"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// GPGSigner signs files using GPG.
-//
-// s := signing.NewGPGSigner("ABCD1234")
-type GPGSigner struct {
- KeyID string
-}
-
-// Compile-time interface check.
-var _ Signer = (*GPGSigner)(nil)
-
-// NewGPGSigner creates a new GPG signer.
-//
-// s := signing.NewGPGSigner("ABCD1234")
-func NewGPGSigner(keyID string) *GPGSigner {
- return &GPGSigner{KeyID: keyID}
-}
-
-// Name returns "gpg".
-//
-// name := s.Name() // → "gpg"
-func (s *GPGSigner) Name() string {
- return "gpg"
-}
-
-// Available checks if gpg is installed and key is configured.
-//
-// ok := s.Available() // → true if gpg is in PATH and key is set
-func (s *GPGSigner) Available() bool {
- if s.KeyID == "" {
- return false
- }
- return resolveGpgCli().OK
-}
-
-// Sign creates a detached ASCII-armored signature.
-// For file.txt, creates file.txt.asc
-//
-// err := s.Sign(ctx, storage.Local, "dist/CHECKSUMS.txt") // creates CHECKSUMS.txt.asc
-func (s *GPGSigner) Sign(ctx context.Context, fs storage.Medium, file string) core.Result {
- if s.KeyID == "" {
- return core.Fail(core.E("gpg.Sign", "gpg not available or key not configured", nil))
- }
-
- gpgCommand := resolveGpgCli()
- if !gpgCommand.OK {
- return core.Fail(core.E("gpg.Sign", "gpg not available or key not configured", core.NewError(gpgCommand.Error())))
- }
-
- output := ax.CombinedOutput(ctx, "", nil, gpgCommand.Value.(string),
- "--detach-sign",
- "--armor",
- "--local-user", s.KeyID,
- "--output", file+".asc",
- file,
- )
- if !output.OK {
- return core.Fail(core.E("gpg.Sign", output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func resolveGpgCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- "/usr/local/bin/gpg",
- "/opt/homebrew/bin/gpg",
- "/usr/local/MacGPG2/bin/gpg",
- }
- }
-
- command := ax.ResolveCommand("gpg", paths...)
- if !command.OK {
- return core.Fail(core.E("gpg.resolveGpgCli", "gpg CLI not found. Install it from https://gnupg.org/download/", core.NewError(command.Error())))
- }
-
- return command
-}
diff --git a/pkg/build/signing/gpg_example_test.go b/pkg/build/signing/gpg_example_test.go
deleted file mode 100644
index b4d10b9..0000000
--- a/pkg/build/signing/gpg_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package signing
-
-import core "dappco.re/go"
-
-// ExampleNewGPGSigner references NewGPGSigner on this package API surface.
-func ExampleNewGPGSigner() {
- _ = NewGPGSigner
- core.Println("NewGPGSigner")
- // Output: NewGPGSigner
-}
-
-// ExampleGPGSigner_Name references GPGSigner.Name on this package API surface.
-func ExampleGPGSigner_Name() {
- _ = (*GPGSigner).Name
- core.Println("GPGSigner.Name")
- // Output: GPGSigner.Name
-}
-
-// ExampleGPGSigner_Available references GPGSigner.Available on this package API surface.
-func ExampleGPGSigner_Available() {
- _ = (*GPGSigner).Available
- core.Println("GPGSigner.Available")
- // Output: GPGSigner.Available
-}
-
-// ExampleGPGSigner_Sign references GPGSigner.Sign on this package API surface.
-func ExampleGPGSigner_Sign() {
- _ = (*GPGSigner).Sign
- core.Println("GPGSigner.Sign")
- // Output: GPGSigner.Sign
-}
diff --git a/pkg/build/signing/gpg_test.go b/pkg/build/signing/gpg_test.go
deleted file mode 100644
index 7296a1d..0000000
--- a/pkg/build/signing/gpg_test.go
+++ /dev/null
@@ -1,209 +0,0 @@
-package signing
-
-import (
- "context"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func TestGPG_GPGSignerNameGood(t *testing.T) {
- s := NewGPGSigner("ABCD1234")
- if !stdlibAssertEqual("gpg", s.Name()) {
- t.Fatalf("want %v, got %v", "gpg", s.Name())
- }
-
-}
-
-func TestGPG_GPGSignerAvailableGood(t *testing.T) {
- s := NewGPGSigner("ABCD1234")
- available := s.Available()
- if available && s.Name() == "" {
- t.Fatal("expected available signer to have a name")
- }
- if !stdlibAssertEqual("gpg", s.Name()) {
- t.Fatalf("want %v, got %v", "gpg", s.Name())
- }
-}
-
-func TestGPG_GPGSignerNoKeyBad(t *testing.T) {
- s := NewGPGSigner("")
- if s.Available() {
- t.Fatal("expected false")
- }
-
-}
-
-func TestGPG_GPGSignerSignBad(t *testing.T) {
- fs := storage.Local
- t.Run("fails when no key", func(t *testing.T) {
- s := NewGPGSigner("")
- result := s.Sign(context.Background(), fs, "test.txt")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "not available or key not configured") {
- t.Fatalf("expected %v to contain %v", result.Error(), "not available or key not configured")
- }
-
- })
-}
-
-func TestGPG_ResolveGpgCliGood(t *testing.T) {
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "gpg")
- if result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- result := resolveGpgCli(fallbackPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- command := result.Value.(string)
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestGPG_ResolveGpgCliBad(t *testing.T) {
- t.Setenv("PATH", "")
-
- result := resolveGpgCli(ax.Join(t.TempDir(), "missing-gpg"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "gpg CLI not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "gpg CLI not found")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestGpg_NewGPGSigner_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewGPGSigner("agent")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestGpg_NewGPGSigner_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewGPGSigner("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestGpg_NewGPGSigner_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = NewGPGSigner("agent")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestGpg_GPGSigner_Name_Good(t *core.T) {
- subject := &GPGSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestGpg_GPGSigner_Name_Bad(t *core.T) {
- subject := &GPGSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestGpg_GPGSigner_Name_Ugly(t *core.T) {
- subject := &GPGSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestGpg_GPGSigner_Available_Good(t *core.T) {
- subject := &GPGSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Available()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestGpg_GPGSigner_Available_Bad(t *core.T) {
- subject := &GPGSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Available()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestGpg_GPGSigner_Available_Ugly(t *core.T) {
- subject := &GPGSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Available()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestGpg_GPGSigner_Sign_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &GPGSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Sign(ctx, storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestGpg_GPGSigner_Sign_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &GPGSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Sign(ctx, storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestGpg_GPGSigner_Sign_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &GPGSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Sign(ctx, storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/signing/sign.go b/pkg/build/signing/sign.go
deleted file mode 100644
index 2b9c9c8..0000000
--- a/pkg/build/signing/sign.go
+++ /dev/null
@@ -1,125 +0,0 @@
-package signing
-
-import (
- "context"
- "runtime"
-
- "dappco.re/go"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// Artifact represents a build output that can be signed.
-// This mirrors build.Artifact to avoid import cycles.
-//
-// a := signing.Artifact{Path: "dist/myapp", OS: "darwin", Arch: "arm64"}
-type Artifact struct {
- Path string
- OS string
- Arch string
-}
-
-// SignBinaries signs binaries for the current host OS in the artifacts list.
-// On macOS it signs darwin artifacts with codesign; on Windows it signs windows
-// artifacts with signtool when the relevant credentials are configured.
-//
-// err := signing.SignBinaries(ctx, storage.Local, cfg, artifacts)
-func SignBinaries(ctx context.Context, fs storage.Medium, cfg SignConfig, artifacts []Artifact) core.Result {
- if !cfg.Enabled {
- return core.Ok(nil)
- }
-
- var signer Signer
- var targetOS string
-
- switch runtime.GOOS {
- case "darwin":
- signer = NewMacOSSigner(cfg.MacOS)
- targetOS = "darwin"
- case "windows":
- signer = NewWindowsSigner(cfg.Windows)
- targetOS = "windows"
- default:
- return core.Ok(nil)
- }
-
- if !signer.Available() {
- return core.Ok(nil) // Silently skip if not configured
- }
-
- return signArtifactsWithSigner(ctx, fs, signer, targetOS, artifacts)
-}
-
-// NotarizeBinaries notarizes macOS binaries if enabled.
-//
-// err := signing.NotarizeBinaries(ctx, storage.Local, cfg, artifacts)
-func NotarizeBinaries(ctx context.Context, fs storage.Medium, cfg SignConfig, artifacts []Artifact) core.Result {
- if !cfg.Enabled || !cfg.MacOS.Notarize {
- return core.Ok(nil)
- }
-
- if runtime.GOOS != "darwin" {
- return core.Ok(nil)
- }
- if len(artifacts) == 0 {
- return core.Ok(nil)
- }
-
- signer := NewMacOSSigner(cfg.MacOS)
- if !signer.Available() {
- return core.Fail(core.E("signing.NotarizeBinaries", "notarization requested but codesign not available", nil))
- }
-
- for _, artifact := range artifacts {
- if artifact.OS != "darwin" {
- continue
- }
-
- core.Print(nil, " Notarizing %s (this may take a few minutes)...", artifact.Path)
- notarized := signer.Notarize(ctx, fs, artifact.Path)
- if !notarized.OK {
- return core.Fail(core.E("signing.NotarizeBinaries", "failed to notarize "+artifact.Path, core.NewError(notarized.Error())))
- }
- }
-
- return core.Ok(nil)
-}
-
-// SignChecksums signs the checksums file with GPG.
-//
-// err := signing.SignChecksums(ctx, storage.Local, cfg, "dist/CHECKSUMS.txt")
-func SignChecksums(ctx context.Context, fs storage.Medium, cfg SignConfig, checksumFile string) core.Result {
- if !cfg.Enabled {
- return core.Ok(nil)
- }
-
- signer := NewGPGSigner(cfg.GPG.Key)
- if !signer.Available() {
- return core.Ok(nil) // Silently skip if not configured
- }
-
- core.Print(nil, " Signing %s with GPG...", checksumFile)
- signed := signer.Sign(ctx, fs, checksumFile)
- if !signed.OK {
- return core.Fail(core.E("signing.SignChecksums", "failed to sign checksums file "+checksumFile, core.NewError(signed.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func signArtifactsWithSigner(ctx context.Context, fs storage.Medium, signer Signer, targetOS string, artifacts []Artifact) core.Result {
- _ = fs
-
- for _, artifact := range artifacts {
- if artifact.OS != targetOS {
- continue
- }
-
- core.Print(nil, " Signing %s...", artifact.Path)
- signed := signer.Sign(ctx, fs, artifact.Path)
- if !signed.OK {
- return core.Fail(core.E("signing.SignBinaries", "failed to sign "+artifact.Path, core.NewError(signed.Error())))
- }
- }
-
- return core.Ok(nil)
-}
diff --git a/pkg/build/signing/sign_example_test.go b/pkg/build/signing/sign_example_test.go
deleted file mode 100644
index 1ec67df..0000000
--- a/pkg/build/signing/sign_example_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package signing
-
-import core "dappco.re/go"
-
-// ExampleSignBinaries references SignBinaries on this package API surface.
-func ExampleSignBinaries() {
- _ = SignBinaries
- core.Println("SignBinaries")
- // Output: SignBinaries
-}
-
-// ExampleNotarizeBinaries references NotarizeBinaries on this package API surface.
-func ExampleNotarizeBinaries() {
- _ = NotarizeBinaries
- core.Println("NotarizeBinaries")
- // Output: NotarizeBinaries
-}
-
-// ExampleSignChecksums references SignChecksums on this package API surface.
-func ExampleSignChecksums() {
- _ = SignChecksums
- core.Println("SignChecksums")
- // Output: SignChecksums
-}
diff --git a/pkg/build/signing/sign_test.go b/pkg/build/signing/sign_test.go
deleted file mode 100644
index 0253f94..0000000
--- a/pkg/build/signing/sign_test.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package signing
-
-import (
- "context"
-
- core "dappco.re/go"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-func TestSign_SignBinaries_Good(t *core.T) {
- cfg := SignConfig{Enabled: false}
- result := SignBinaries(context.Background(), coreio.NewMemoryMedium(), cfg, []Artifact{{Path: "dist/app", OS: "linux", Arch: "amd64"}})
- core.AssertTrue(t, result.OK)
- core.AssertEqual(t, false, cfg.Enabled)
-}
-
-func TestSign_SignBinaries_Bad(t *core.T) {
- cfg := SignConfig{Enabled: true}
- result := SignBinaries(context.Background(), coreio.NewMemoryMedium(), cfg, nil)
- core.AssertTrue(t, result.OK)
- core.AssertEqual(t, true, cfg.Enabled)
-}
-
-func TestSign_SignBinaries_Ugly(t *core.T) {
- artifacts := []Artifact{{}}
- result := SignBinaries(context.Background(), nil, SignConfig{Enabled: false}, artifacts)
- core.AssertTrue(t, result.OK)
- core.AssertLen(t, artifacts, 1)
-}
-
-func TestSign_NotarizeBinaries_Good(t *core.T) {
- cfg := SignConfig{Enabled: false}
- result := NotarizeBinaries(context.Background(), coreio.NewMemoryMedium(), cfg, []Artifact{{Path: "dist/app.zip", OS: "darwin", Arch: "arm64"}})
- core.AssertTrue(t, result.OK)
- core.AssertEqual(t, false, cfg.Enabled)
-}
-
-func TestSign_NotarizeBinaries_Bad(t *core.T) {
- cfg := SignConfig{Enabled: true}
- result := NotarizeBinaries(context.Background(), coreio.NewMemoryMedium(), cfg, nil)
- core.AssertTrue(t, result.OK)
- core.AssertEqual(t, true, cfg.Enabled)
-}
-
-func TestSign_NotarizeBinaries_Ugly(t *core.T) {
- cfg := SignConfig{Enabled: true, MacOS: MacOSConfig{Notarize: false}}
- result := NotarizeBinaries(context.Background(), coreio.NewMemoryMedium(), cfg, []Artifact{{Path: "dist/app.zip", OS: "darwin"}})
- core.AssertTrue(t, result.OK)
- core.AssertFalse(t, cfg.MacOS.Notarize)
-}
-
-func TestSign_SignChecksums_Good(t *core.T) {
- cfg := SignConfig{Enabled: false}
- result := SignChecksums(context.Background(), coreio.NewMemoryMedium(), cfg, "CHECKSUMS.txt")
- core.AssertTrue(t, result.OK)
- core.AssertEqual(t, false, cfg.Enabled)
-}
-
-func TestSign_SignChecksums_Bad(t *core.T) {
- cfg := SignConfig{Enabled: true}
- result := SignChecksums(context.Background(), coreio.NewMemoryMedium(), cfg, "")
- core.AssertTrue(t, result.OK)
- core.AssertEqual(t, true, cfg.Enabled)
-}
-
-func TestSign_SignChecksums_Ugly(t *core.T) {
- checksumFile := ""
- result := SignChecksums(context.Background(), nil, SignConfig{Enabled: false}, checksumFile)
- core.AssertTrue(t, result.OK)
- core.AssertEmpty(t, checksumFile)
-}
diff --git a/pkg/build/signing/signer.go b/pkg/build/signing/signer.go
deleted file mode 100644
index 716feb2..0000000
--- a/pkg/build/signing/signer.go
+++ /dev/null
@@ -1,160 +0,0 @@
-// Package signing provides code signing for build artifacts.
-package signing
-
-import (
- "context"
-
- "dappco.re/go"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// Signer defines the interface for code signing implementations.
-//
-// var s signing.Signer = signing.NewGPGSigner(keyID)
-// err := s.Sign(ctx, storage.Local, "dist/myapp")
-type Signer interface {
- // Name returns the signer's identifier.
- Name() string
- // Available checks if this signer can be used.
- Available() bool
- // Sign signs the artifact at the given path.
- Sign(ctx context.Context, fs storage.Medium, path string) core.Result
-}
-
-// SignConfig holds signing configuration from .core/build.yaml.
-//
-// cfg := signing.DefaultSignConfig()
-type SignConfig struct {
- Enabled bool `json:"enabled" yaml:"enabled"`
- GPG GPGConfig `json:"gpg,omitempty" yaml:"gpg,omitempty"`
- MacOS MacOSConfig `json:"macos,omitempty" yaml:"macos,omitempty"`
- Windows WindowsConfig `json:"windows,omitempty" yaml:"windows,omitempty"`
-}
-
-// GPGConfig holds GPG signing configuration.
-//
-// cfg := signing.GPGConfig{Key: "ABCD1234"}
-type GPGConfig struct {
- Key string `json:"key" yaml:"key"` // Key ID or fingerprint, supports $ENV
-}
-
-// MacOSConfig holds macOS codesign configuration.
-//
-// cfg := signing.MacOSConfig{Identity: "Developer ID Application: Acme Inc (TEAM123)"}
-type MacOSConfig struct {
- Identity string `json:"identity" yaml:"identity"` // Developer ID Application: ...
- Notarize bool `json:"notarize" yaml:"notarize"` // Submit to Apple for notarization
- AppleID string `json:"apple_id" yaml:"apple_id"` // Apple account email
- TeamID string `json:"team_id" yaml:"team_id"` // Team ID
- AppPassword string `json:"app_password" yaml:"app_password"` // App-specific password
-}
-
-// WindowsConfig holds Windows signtool configuration.
-//
-// cfg := signing.WindowsConfig{Certificate: "cert.pfx", Password: "secret"}
-type WindowsConfig struct {
- Signtool bool `json:"signtool" yaml:"signtool"` // Enable/disable signtool integration.
- Certificate string `json:"certificate" yaml:"certificate"` // Path to .pfx
- Password string `json:"password" yaml:"password"` // Certificate password
-
- signtoolExplicit bool `json:"-" yaml:"-"`
-}
-
-// DefaultSignConfig returns sensible defaults.
-//
-// cfg := signing.DefaultSignConfig()
-func DefaultSignConfig() SignConfig {
- return SignConfig{
- Enabled: true,
- GPG: GPGConfig{
- Key: core.Env("GPG_KEY_ID"),
- },
- MacOS: MacOSConfig{
- Identity: core.Env("CODESIGN_IDENTITY"),
- AppleID: core.Env("APPLE_ID"),
- TeamID: core.Env("APPLE_TEAM_ID"),
- AppPassword: core.Env("APPLE_APP_PASSWORD"),
- },
- Windows: WindowsConfig{
- Signtool: true,
- Certificate: core.Env("SIGNTOOL_CERTIFICATE"),
- Password: core.Env("SIGNTOOL_PASSWORD"),
- },
- }
-}
-
-// ExpandEnv expands environment variables in config values.
-//
-// cfg.ExpandEnv() // expands $GPG_KEY_ID, $CODESIGN_IDENTITY etc.
-func (c *SignConfig) ExpandEnv() {
- c.GPG.Key = expandEnv(c.GPG.Key)
- c.MacOS.Identity = expandEnv(c.MacOS.Identity)
- c.MacOS.AppleID = expandEnv(c.MacOS.AppleID)
- c.MacOS.TeamID = expandEnv(c.MacOS.TeamID)
- c.MacOS.AppPassword = expandEnv(c.MacOS.AppPassword)
- c.Windows.Certificate = expandEnv(c.Windows.Certificate)
- c.Windows.Password = expandEnv(c.Windows.Password)
-}
-
-func (c WindowsConfig) signtoolEnabled() bool {
- if c.signtoolExplicit {
- return c.Signtool
- }
- return true
-}
-
-// SetSigntool records an explicit signtool preference from config.
-func (c *WindowsConfig) SetSigntool(enabled bool) {
- if c == nil {
- return
- }
- c.Signtool = enabled
- c.signtoolExplicit = true
-}
-
-// expandEnv expands $VAR or ${VAR} in a string.
-func expandEnv(s string) string {
- if !core.Contains(s, "$") {
- return s
- }
-
- buf := core.NewBuilder()
- for i := 0; i < len(s); {
- if s[i] != '$' {
- buf.WriteByte(s[i])
- i++
- continue
- }
-
- if i+1 < len(s) && s[i+1] == '{' {
- j := i + 2
- for j < len(s) && s[j] != '}' {
- j++
- }
- if j < len(s) {
- buf.WriteString(core.Env(s[i+2 : j]))
- i = j + 1
- continue
- }
- }
-
- j := i + 1
- for j < len(s) {
- c := s[j]
- if c != '_' && (c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') {
- break
- }
- j++
- }
- if j > i+1 {
- buf.WriteString(core.Env(s[i+1 : j]))
- i = j
- continue
- }
-
- buf.WriteByte(s[i])
- i++
- }
-
- return buf.String()
-}
diff --git a/pkg/build/signing/signer_example_test.go b/pkg/build/signing/signer_example_test.go
deleted file mode 100644
index 8ab2100..0000000
--- a/pkg/build/signing/signer_example_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package signing
-
-import core "dappco.re/go"
-
-// ExampleDefaultSignConfig references DefaultSignConfig on this package API surface.
-func ExampleDefaultSignConfig() {
- _ = DefaultSignConfig
- core.Println("DefaultSignConfig")
- // Output: DefaultSignConfig
-}
-
-// ExampleSignConfig_ExpandEnv references SignConfig.ExpandEnv on this package API surface.
-func ExampleSignConfig_ExpandEnv() {
- _ = (*SignConfig).ExpandEnv
- core.Println("SignConfig.ExpandEnv")
- // Output: SignConfig.ExpandEnv
-}
-
-// ExampleWindowsConfig_SetSigntool references WindowsConfig.SetSigntool on this package API surface.
-func ExampleWindowsConfig_SetSigntool() {
- _ = (*WindowsConfig).SetSigntool
- core.Println("WindowsConfig.SetSigntool")
- // Output: WindowsConfig.SetSigntool
-}
diff --git a/pkg/build/signing/signer_test.go b/pkg/build/signing/signer_test.go
deleted file mode 100644
index 4faaa0b..0000000
--- a/pkg/build/signing/signer_test.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package signing
-
-import core "dappco.re/go"
-
-func TestSigner_DefaultSignConfig_Good(t *core.T) {
- clearSigningEnv(t, "GPG_KEY_ID")
- setSigningEnv(t, "GPG_KEY_ID", "ABC123")
- defer clearSigningEnv(t, "GPG_KEY_ID")
-
- cfg := DefaultSignConfig()
- core.AssertTrue(t, cfg.Enabled)
- core.AssertEqual(t, "ABC123", cfg.GPG.Key)
-}
-
-func TestSigner_DefaultSignConfig_Bad(t *core.T) {
- clearSigningEnv(t, "GPG_KEY_ID", "SIGNTOOL_CERTIFICATE")
- cfg := DefaultSignConfig()
- core.AssertTrue(t, cfg.Windows.Signtool)
- core.AssertEqual(t, "", cfg.GPG.Key)
-}
-
-func TestSigner_DefaultSignConfig_Ugly(t *core.T) {
- clearSigningEnv(t, "APPLE_TEAM_ID")
- setSigningEnv(t, "APPLE_TEAM_ID", "TEAM123")
- defer clearSigningEnv(t, "APPLE_TEAM_ID")
-
- cfg := DefaultSignConfig()
- core.AssertEqual(t, "TEAM123", cfg.MacOS.TeamID)
-}
-
-func TestSigner_SignConfig_ExpandEnv_Good(t *core.T) {
- clearSigningEnv(t, "GPG_KEY_ID")
- setSigningEnv(t, "GPG_KEY_ID", "ABC123")
- defer clearSigningEnv(t, "GPG_KEY_ID")
-
- cfg := SignConfig{GPG: GPGConfig{Key: "$GPG_KEY_ID"}}
- cfg.ExpandEnv()
- core.AssertEqual(t, "ABC123", cfg.GPG.Key)
-}
-
-func TestSigner_SignConfig_ExpandEnv_Bad(t *core.T) {
- cfg := SignConfig{GPG: GPGConfig{Key: "$"}}
- cfg.ExpandEnv()
- core.AssertEqual(t, "$", cfg.GPG.Key)
-}
-
-func TestSigner_SignConfig_ExpandEnv_Ugly(t *core.T) {
- clearSigningEnv(t, "SIGNTOOL_PASSWORD")
- setSigningEnv(t, "SIGNTOOL_PASSWORD", "secret")
- defer clearSigningEnv(t, "SIGNTOOL_PASSWORD")
-
- cfg := SignConfig{Windows: WindowsConfig{Password: "${SIGNTOOL_PASSWORD}"}}
- cfg.ExpandEnv()
- core.AssertEqual(t, "secret", cfg.Windows.Password)
-}
-
-func TestSigner_WindowsConfig_SetSigntool_Good(t *core.T) {
- cfg := WindowsConfig{}
- cfg.SetSigntool(false)
- core.AssertFalse(t, cfg.signtoolEnabled())
-}
-
-func TestSigner_WindowsConfig_SetSigntool_Bad(t *core.T) {
- var cfg *WindowsConfig
- core.AssertNotPanics(t, func() {
- cfg.SetSigntool(false)
- })
- core.AssertNil(t, cfg)
-}
-
-func TestSigner_WindowsConfig_SetSigntool_Ugly(t *core.T) {
- cfg := WindowsConfig{}
- core.AssertTrue(t, cfg.signtoolEnabled())
- cfg.SetSigntool(true)
- core.AssertTrue(t, cfg.signtoolEnabled())
-}
-
-func setSigningEnv(t *core.T, key, value string) {
- t.Helper()
- setenv := core.Setenv
- r := setenv(key, value)
- core.RequireTrue(t, r.OK, r.Error())
-}
-
-func clearSigningEnv(t *core.T, keys ...string) {
- t.Helper()
- unsetenv := core.Unsetenv
- for _, key := range keys {
- r := unsetenv(key)
- core.RequireTrue(t, r.OK, r.Error())
- }
-}
diff --git a/pkg/build/signing/signing_test.go b/pkg/build/signing/signing_test.go
deleted file mode 100644
index 5404532..0000000
--- a/pkg/build/signing/signing_test.go
+++ /dev/null
@@ -1,486 +0,0 @@
-package signing
-
-import (
- "context"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/testassert"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func TestSigning_SignBinariesSkipsNonDarwinGood(t *testing.T) {
- ctx := context.Background()
- fs := storage.Local
- cfg := SignConfig{
- Enabled: true,
- MacOS: MacOSConfig{
- Identity: "Developer ID Application: Test",
- },
- }
-
- // Create fake artifact for linux
- artifacts := []Artifact{
- {Path: "/tmp/test-binary", OS: "linux", Arch: "amd64"},
- }
-
- // Should not error even though binary doesn't exist (skips non-darwin)
- result := SignBinaries(ctx, fs, cfg, artifacts)
- if !result.OK {
- t.Errorf("unexpected error: %v", result.Error())
- }
-}
-
-func TestSigning_SignBinariesDisabledConfigGood(t *testing.T) {
- ctx := context.Background()
- fs := storage.Local
- cfg := SignConfig{
- Enabled: false,
- }
-
- artifacts := []Artifact{
- {Path: "/tmp/test-binary", OS: "darwin", Arch: "arm64"},
- }
-
- result := SignBinaries(ctx, fs, cfg, artifacts)
- if !result.OK {
- t.Errorf("unexpected error: %v", result.Error())
- }
-}
-
-func TestSigning_SignBinariesSkipsOnNonMacOSGood(t *testing.T) {
- if runtime.GOOS == "darwin" {
- t.Skip("Skipping on macOS - this tests non-macOS behavior")
- }
-
- ctx := context.Background()
- fs := storage.Local
- cfg := SignConfig{
- Enabled: true,
- MacOS: MacOSConfig{
- Identity: "Developer ID Application: Test",
- },
- }
-
- artifacts := []Artifact{
- {Path: "/tmp/test-binary", OS: "darwin", Arch: "arm64"},
- }
-
- result := SignBinaries(ctx, fs, cfg, artifacts)
- if !result.OK {
- t.Errorf("unexpected error: %v", result.Error())
- }
-}
-
-func TestSigning_NotarizeBinariesDisabledConfigGood(t *testing.T) {
- ctx := context.Background()
- fs := storage.Local
- cfg := SignConfig{
- Enabled: false,
- }
-
- artifacts := []Artifact{
- {Path: "/tmp/test-binary", OS: "darwin", Arch: "arm64"},
- }
-
- result := NotarizeBinaries(ctx, fs, cfg, artifacts)
- if !result.OK {
- t.Errorf("unexpected error: %v", result.Error())
- }
-}
-
-func TestSigning_NotarizeBinariesNotarizeDisabledGood(t *testing.T) {
- ctx := context.Background()
- fs := storage.Local
- cfg := SignConfig{
- Enabled: true,
- MacOS: MacOSConfig{
- Notarize: false,
- },
- }
-
- artifacts := []Artifact{
- {Path: "/tmp/test-binary", OS: "darwin", Arch: "arm64"},
- }
-
- result := NotarizeBinaries(ctx, fs, cfg, artifacts)
- if !result.OK {
- t.Errorf("unexpected error: %v", result.Error())
- }
-}
-
-func TestSigning_SignChecksumsSkipsNoKeyGood(t *testing.T) {
- ctx := context.Background()
- fs := storage.Local
- cfg := SignConfig{
- Enabled: true,
- GPG: GPGConfig{
- Key: "", // No key configured
- },
- }
-
- // Should silently skip when no key
- result := SignChecksums(ctx, fs, cfg, "/tmp/CHECKSUMS.txt")
- if !result.OK {
- t.Errorf("unexpected error: %v", result.Error())
- }
-}
-
-func TestSigning_SignChecksumsDisabledGood(t *testing.T) {
- ctx := context.Background()
- fs := storage.Local
- cfg := SignConfig{
- Enabled: false,
- }
-
- result := SignChecksums(ctx, fs, cfg, "/tmp/CHECKSUMS.txt")
- if !result.OK {
- t.Errorf("unexpected error: %v", result.Error())
- }
-}
-
-func TestSigning_DefaultSignConfig_Good(t *testing.T) {
- cfg := DefaultSignConfig()
- if !(cfg.Enabled) {
- t.Fatal("expected true")
- }
- if !(cfg.Windows.Signtool) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestSigning_SignConfigExpandEnvGood(t *testing.T) {
- t.Setenv("TEST_KEY", "ABC")
- cfg := SignConfig{
- GPG: GPGConfig{Key: "$TEST_KEY"},
- }
- cfg.ExpandEnv()
- if !stdlibAssertEqual("ABC", cfg.GPG.Key) {
- t.Fatalf("want %v, got %v", "ABC", cfg.GPG.Key)
- }
-
-}
-
-func TestSigning_WindowsSignerGood(t *testing.T) {
- fs := storage.Local
- s := NewWindowsSigner(WindowsConfig{Signtool: true, Certificate: "cert.pfx"})
- if !stdlibAssertEqual("signtool", s.Name()) {
- t.Fatalf("want %v, got %v", "signtool", s.Name())
- }
-
- if runtime.GOOS != "windows" {
- if s.Available() {
- t.Fatal("expected false")
- }
- if s.Sign(context.Background(), fs, "test.exe").OK {
- t.Fatal("expected error")
-
- // On Windows, availability depends on the SDK toolchain being installed.
- }
-
- return
- }
-
- _ = s.Available()
-}
-
-func TestSigning_WindowsSignerHonoursSigntoolToggleGood(t *testing.T) {
- s := NewWindowsSigner(WindowsConfig{
- Signtool: false,
- Certificate: "cert.pfx",
- signtoolExplicit: true,
- })
- if s.Available() {
- t.Fatal("expected false")
-
- // mockSigner is a test double that records calls to Sign.
- }
-
-}
-
-type mockSigner struct {
- name string
- available bool
- signedPaths []string
- signError error
-}
-
-func (m *mockSigner) Name() string {
- return m.name
-}
-
-func (m *mockSigner) Available() bool {
- return m.available
-}
-
-func (m *mockSigner) Sign(ctx context.Context, fs storage.Medium, path string) core.Result {
- m.signedPaths = append(m.signedPaths, path)
- if m.signError != nil {
- return core.Fail(m.signError)
- }
- return core.Ok(nil)
-}
-
-// Verify mockSigner implements Signer
-var _ Signer = (*mockSigner)(nil)
-
-func TestSigning_SignBinariesMockSignerGood(t *testing.T) {
- t.Run("signs only darwin artifacts", func(t *testing.T) {
- artifacts := []Artifact{
- {Path: "/dist/linux_amd64/myapp", OS: "linux", Arch: "amd64"},
- {Path: "/dist/darwin_arm64/myapp", OS: "darwin", Arch: "arm64"},
- {Path: "/dist/windows_amd64/myapp.exe", OS: "windows", Arch: "amd64"},
- {Path: "/dist/darwin_amd64/myapp", OS: "darwin", Arch: "amd64"},
- }
-
- // SignBinaries filters to darwin only and calls signer.Sign for each.
- // We can verify the logic by checking that non-darwin artifacts are skipped.
- // Since SignBinaries uses NewMacOSSigner internally, we test the filtering
- // by passing only darwin artifacts and confirming non-darwin are skipped.
- cfg := SignConfig{
- Enabled: true,
- MacOS: MacOSConfig{Identity: ""},
- }
-
- // With empty identity, Available() returns false, so Sign is never called.
- // This verifies the short-circuit behavior.
- ctx := context.Background()
- result := SignBinaries(ctx, storage.Local, cfg, artifacts)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- })
-
- t.Run("skips all when enabled is false", func(t *testing.T) {
- artifacts := []Artifact{
- {Path: "/dist/darwin_arm64/myapp", OS: "darwin", Arch: "arm64"},
- }
-
- cfg := SignConfig{Enabled: false}
- result := SignBinaries(context.Background(), storage.Local, cfg, artifacts)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- })
-
- t.Run("handles empty artifact list", func(t *testing.T) {
- cfg := SignConfig{
- Enabled: true,
- MacOS: MacOSConfig{Identity: "Developer ID"},
- }
- result := SignBinaries(context.Background(), storage.Local, cfg, []Artifact{})
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- })
-}
-
-func TestSigning_signArtifactsWithSigner_Good(t *testing.T) {
- signer := &mockSigner{name: "mock", available: true}
- artifacts := []Artifact{
- {Path: "/dist/linux_amd64/myapp", OS: "linux", Arch: "amd64"},
- {Path: "/dist/windows_amd64/myapp.exe", OS: "windows", Arch: "amd64"},
- {Path: "/dist/windows_arm64/myapp.exe", OS: "windows", Arch: "arm64"},
- }
-
- result := signArtifactsWithSigner(context.Background(), storage.Local, signer, "windows", artifacts)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- if !stdlibAssertEqual([]string{"/dist/windows_amd64/myapp.exe", "/dist/windows_arm64/myapp.exe"}, signer.signedPaths) {
- t.Fatalf("want %v, got %v", []string{"/dist/windows_amd64/myapp.exe", "/dist/windows_arm64/myapp.exe"}, signer.signedPaths)
- }
-
-}
-
-func TestSigning_ResolveSigntoolCliGood(t *testing.T) {
- fallbackDir := t.TempDir()
- fallbackPath := fallbackDir + "/signtool.exe"
- if result := storage.Local.Write(fallbackPath, "#!/bin/sh\nexit 0\n"); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- commandResult := resolveSigntoolCli(fallbackPath)
- if !commandResult.OK {
- t.Fatalf("unexpected error: %v", commandResult.Error())
- }
- command := commandResult.Value.(string)
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestSigning_ResolveSigntoolCliBad(t *testing.T) {
- t.Setenv("PATH", "")
-
- result := resolveSigntoolCli(t.TempDir() + "/missing-signtool.exe")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "signtool tool not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "signtool tool not found")
- }
-
-}
-
-func TestSigning_SignChecksumsMockSignerGood(t *testing.T) {
- t.Run("skips when GPG key is empty", func(t *testing.T) {
- cfg := SignConfig{
- Enabled: true,
- GPG: GPGConfig{Key: ""},
- }
-
- result := SignChecksums(context.Background(), storage.Local, cfg, "/tmp/CHECKSUMS.txt")
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- })
-
- t.Run("skips when disabled", func(t *testing.T) {
- cfg := SignConfig{
- Enabled: false,
- GPG: GPGConfig{Key: "ABCD1234"},
- }
-
- result := SignChecksums(context.Background(), storage.Local, cfg, "/tmp/CHECKSUMS.txt")
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- })
-}
-
-func TestSigning_NotarizeBinariesMockSignerGood(t *testing.T) {
- t.Run("skips when notarize is false", func(t *testing.T) {
- cfg := SignConfig{
- Enabled: true,
- MacOS: MacOSConfig{Notarize: false},
- }
-
- artifacts := []Artifact{
- {Path: "/dist/darwin_arm64/myapp", OS: "darwin", Arch: "arm64"},
- }
-
- result := NotarizeBinaries(context.Background(), storage.Local, cfg, artifacts)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- })
-
- t.Run("skips when disabled", func(t *testing.T) {
- cfg := SignConfig{
- Enabled: false,
- MacOS: MacOSConfig{Notarize: true},
- }
-
- artifacts := []Artifact{
- {Path: "/dist/darwin_arm64/myapp", OS: "darwin", Arch: "arm64"},
- }
-
- result := NotarizeBinaries(context.Background(), storage.Local, cfg, artifacts)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- })
-
- t.Run("handles empty artifact list", func(t *testing.T) {
- cfg := SignConfig{
- Enabled: true,
- MacOS: MacOSConfig{Notarize: true, Identity: "Dev ID"},
- }
-
- result := NotarizeBinaries(context.Background(), storage.Local, cfg, []Artifact{})
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- })
-}
-
-func TestSigning_ExpandEnv_Good(t *testing.T) {
- t.Run("expands all config fields", func(t *testing.T) {
- t.Setenv("TEST_GPG_KEY", "GPG123")
- t.Setenv("TEST_IDENTITY", "Developer ID Application: Test")
- t.Setenv("TEST_APPLE_ID", "test@apple.com")
- t.Setenv("TEST_TEAM_ID", "TEAM123")
- t.Setenv("TEST_APP_PASSWORD", "secret")
- t.Setenv("TEST_CERT_PATH", "/path/to/cert.pfx")
- t.Setenv("TEST_CERT_PASS", "certpass")
-
- cfg := SignConfig{
- GPG: GPGConfig{Key: "$TEST_GPG_KEY"},
- MacOS: MacOSConfig{
- Identity: "$TEST_IDENTITY",
- AppleID: "$TEST_APPLE_ID",
- TeamID: "$TEST_TEAM_ID",
- AppPassword: "$TEST_APP_PASSWORD",
- },
- Windows: WindowsConfig{
- Certificate: "$TEST_CERT_PATH",
- Password: "$TEST_CERT_PASS",
- },
- }
-
- cfg.ExpandEnv()
- if !stdlibAssertEqual("GPG123", cfg.GPG.Key) {
- t.Fatalf("want %v, got %v", "GPG123", cfg.GPG.Key)
- }
- if !stdlibAssertEqual("Developer ID Application: Test", cfg.MacOS.Identity) {
- t.Fatalf("want %v, got %v", "Developer ID Application: Test", cfg.MacOS.Identity)
- }
- if !stdlibAssertEqual("test@apple.com", cfg.MacOS.AppleID) {
- t.Fatalf("want %v, got %v", "test@apple.com", cfg.MacOS.AppleID)
- }
- if !stdlibAssertEqual("TEAM123", cfg.MacOS.TeamID) {
- t.Fatalf("want %v, got %v", "TEAM123", cfg.MacOS.TeamID)
- }
- if !stdlibAssertEqual("secret", cfg.MacOS.AppPassword) {
- t.Fatalf("want %v, got %v", "secret", cfg.MacOS.AppPassword)
- }
- if !stdlibAssertEqual("/path/to/cert.pfx", cfg.Windows.Certificate) {
- t.Fatalf("want %v, got %v", "/path/to/cert.pfx", cfg.Windows.Certificate)
- }
- if !stdlibAssertEqual("certpass", cfg.Windows.Password) {
- t.Fatalf("want %v, got %v", "certpass", cfg.Windows.Password)
- }
-
- })
-
- t.Run("preserves non-env values", func(t *testing.T) {
- cfg := SignConfig{
- GPG: GPGConfig{Key: "literal-key"},
- MacOS: MacOSConfig{
- Identity: "Developer ID Application: Literal",
- },
- }
-
- cfg.ExpandEnv()
- if !stdlibAssertEqual("literal-key", cfg.GPG.Key) {
- t.Fatalf("want %v, got %v", "literal-key", cfg.GPG.Key)
- }
- if !stdlibAssertEqual("Developer ID Application: Literal", cfg.MacOS.Identity) {
- t.Fatalf("want %v, got %v", "Developer ID Application: Literal", cfg.MacOS.Identity)
- }
-
- })
-}
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
diff --git a/pkg/build/signing/signtool.go b/pkg/build/signing/signtool.go
deleted file mode 100644
index c8cffc2..0000000
--- a/pkg/build/signing/signtool.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package signing
-
-import (
- "context"
- "runtime"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-// WindowsSigner signs binaries using Windows signtool.
-//
-// s := signing.NewWindowsSigner(cfg.Windows)
-type WindowsSigner struct {
- config WindowsConfig
-}
-
-// Compile-time interface check.
-var _ Signer = (*WindowsSigner)(nil)
-
-// NewWindowsSigner creates a new Windows signer.
-//
-// s := signing.NewWindowsSigner(cfg.Windows)
-func NewWindowsSigner(cfg WindowsConfig) *WindowsSigner {
- return &WindowsSigner{config: cfg}
-}
-
-// Name returns "signtool".
-//
-// name := s.Name() // → "signtool"
-func (s *WindowsSigner) Name() string {
- return "signtool"
-}
-
-// Available checks if running on Windows with signtool and certificate configured.
-//
-// ok := s.Available() // → true if on Windows with certificate configured
-func (s *WindowsSigner) Available() bool {
- if !s.config.signtoolEnabled() {
- return false
- }
- if runtime.GOOS != "windows" {
- return false
- }
- if s.config.Certificate == "" {
- return false
- }
- return resolveSigntoolCli().OK
-}
-
-// Sign signs a binary using signtool and a PFX certificate.
-//
-// err := s.Sign(ctx, storage.Local, "dist/myapp.exe")
-func (s *WindowsSigner) Sign(ctx context.Context, fs storage.Medium, binary string) core.Result {
- _ = fs
-
- if !s.Available() {
- if runtime.GOOS != "windows" {
- return core.Fail(core.E("signtool.Sign", "signtool is only available on Windows", nil))
- }
- if s.config.Certificate == "" {
- return core.Fail(core.E("signtool.Sign", "signtool certificate not configured", nil))
- }
- return core.Fail(core.E("signtool.Sign", "signtool tool not found in PATH", nil))
- }
-
- signtoolCommand := resolveSigntoolCli()
- if !signtoolCommand.OK {
- return core.Fail(core.E("signtool.Sign", "signtool tool not found in PATH", core.NewError(signtoolCommand.Error())))
- }
-
- args := []string{
- "sign",
- "/f", s.config.Certificate,
- "/fd", "sha256",
- "/tr", "http://timestamp.digicert.com",
- "/td", "sha256",
- }
- if s.config.Password != "" {
- args = append(args, "/p", s.config.Password)
- }
- args = append(args, binary)
-
- output := ax.CombinedOutput(ctx, "", nil, signtoolCommand.Value.(string), args...)
- if !output.OK {
- return core.Fail(core.E("signtool.Sign", output.Error(), core.NewError(output.Error())))
- }
-
- return core.Ok(nil)
-}
-
-func resolveSigntoolCli(paths ...string) core.Result {
- if len(paths) == 0 {
- paths = []string{
- `C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x64\\signtool.exe`,
- `C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x86\\signtool.exe`,
- `C:\\Program Files\\Windows Kits\\10\\bin\\x64\\signtool.exe`,
- `C:\\Program Files\\Windows Kits\\10\\bin\\x86\\signtool.exe`,
- }
- }
-
- command := ax.ResolveCommand("signtool", paths...)
- if !command.OK {
- return core.Fail(core.E("signtool.resolveSigntoolCli", "signtool tool not found. Install the Windows SDK.", core.NewError(command.Error())))
- }
-
- return command
-}
diff --git a/pkg/build/signing/signtool_example_test.go b/pkg/build/signing/signtool_example_test.go
deleted file mode 100644
index 8451d6b..0000000
--- a/pkg/build/signing/signtool_example_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package signing
-
-import core "dappco.re/go"
-
-// ExampleNewWindowsSigner references NewWindowsSigner on this package API surface.
-func ExampleNewWindowsSigner() {
- _ = NewWindowsSigner
- core.Println("NewWindowsSigner")
- // Output: NewWindowsSigner
-}
-
-// ExampleWindowsSigner_Name references WindowsSigner.Name on this package API surface.
-func ExampleWindowsSigner_Name() {
- _ = (*WindowsSigner).Name
- core.Println("WindowsSigner.Name")
- // Output: WindowsSigner.Name
-}
-
-// ExampleWindowsSigner_Available references WindowsSigner.Available on this package API surface.
-func ExampleWindowsSigner_Available() {
- _ = (*WindowsSigner).Available
- core.Println("WindowsSigner.Available")
- // Output: WindowsSigner.Available
-}
-
-// ExampleWindowsSigner_Sign references WindowsSigner.Sign on this package API surface.
-func ExampleWindowsSigner_Sign() {
- _ = (*WindowsSigner).Sign
- core.Println("WindowsSigner.Sign")
- // Output: WindowsSigner.Sign
-}
diff --git a/pkg/build/signing/signtool_test.go b/pkg/build/signing/signtool_test.go
deleted file mode 100644
index 86686c3..0000000
--- a/pkg/build/signing/signtool_test.go
+++ /dev/null
@@ -1,226 +0,0 @@
-package signing
-
-import (
- "context"
- "runtime"
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func TestSigntool_NewWindowsSigner_Good(t *testing.T) {
- signer := NewWindowsSigner(WindowsConfig{
- Signtool: true,
- Certificate: "cert.pfx",
- Password: "secret",
- })
- if !stdlibAssertEqual("signtool", signer.Name()) {
- t.Fatalf("want %v, got %v", "signtool", signer.Name())
- }
-
-}
-
-func TestSigntool_NewWindowsSigner_Bad(t *testing.T) {
- t.Run("available is false when the explicit toggle disables signtool", func(t *testing.T) {
- signer := NewWindowsSigner(WindowsConfig{
- Signtool: false,
- Certificate: "cert.pfx",
- signtoolExplicit: true,
- })
- if signer.Available() {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestSigntool_NewWindowsSigner_Ugly(t *testing.T) {
- t.Run("available is false without a certificate", func(t *testing.T) {
- signer := NewWindowsSigner(WindowsConfig{Signtool: true})
- if signer.Available() {
- t.Fatal("expected false")
- }
-
- })
-}
-
-func TestSigntool_Available_Good(t *testing.T) {
- signer := NewWindowsSigner(WindowsConfig{Signtool: true, Certificate: "cert.pfx"})
- if runtime.GOOS != "windows" {
- if signer.Available() {
- t.Fatal("expected signtool to be unavailable on non-Windows hosts")
- }
- return
- }
- if !stdlibAssertEqual("signtool", signer.Name()) {
- t.Fatalf("want %v, got %v", "signtool", signer.Name())
- }
-}
-
-func TestSigntool_Sign_Bad(t *testing.T) {
- t.Run("returns the platform guard on non-Windows hosts", func(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("this assertion is specific to non-Windows hosts")
- }
-
- signer := NewWindowsSigner(WindowsConfig{
- Signtool: true,
- Certificate: "cert.pfx",
- })
-
- result := signer.Sign(context.Background(), storage.Local, "test.exe")
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "only available on Windows") {
- t.Fatalf("expected %v to contain %v", result.Error(), "only available on Windows")
- }
-
- })
-}
-
-func TestSigntool_Sign_Good(t *testing.T) {
- signer := NewWindowsSigner(WindowsConfig{Signtool: true, Certificate: "cert.pfx"})
- result := signer.Sign(context.Background(), storage.Local, "test.exe")
- if runtime.GOOS != "windows" {
- if result.OK {
- t.Fatal("expected non-Windows platform guard")
- }
- return
- }
- if !result.OK && !stdlibAssertContains(result.Error(), "signtool") {
- t.Fatalf("expected signtool-related result, got %v", result.Error())
- }
-}
-
-func TestSigntool_ResolveSigntoolCliGood(t *testing.T) {
- fallbackDir := t.TempDir()
- fallbackPath := ax.Join(fallbackDir, "signtool.exe")
- if result := ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-
- t.Setenv("PATH", "")
-
- result := resolveSigntoolCli(fallbackPath)
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- command := result.Value.(string)
- if !stdlibAssertEqual(fallbackPath, command) {
- t.Fatalf("want %v, got %v", fallbackPath, command)
- }
-
-}
-
-func TestSigntool_ResolveSigntoolCliBad(t *testing.T) {
- t.Setenv("PATH", "")
-
- result := resolveSigntoolCli(ax.Join(t.TempDir(), "missing-signtool.exe"))
- if result.OK {
- t.Fatal("expected error")
- }
- if !stdlibAssertContains(result.Error(), "signtool tool not found") {
- t.Fatalf("expected %v to contain %v", result.Error(), "signtool tool not found")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestSigntool_WindowsSigner_Name_Good(t *core.T) {
- subject := &WindowsSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestSigntool_WindowsSigner_Name_Bad(t *core.T) {
- subject := &WindowsSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestSigntool_WindowsSigner_Name_Ugly(t *core.T) {
- subject := &WindowsSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Name()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestSigntool_WindowsSigner_Available_Good(t *core.T) {
- subject := &WindowsSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Available()
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestSigntool_WindowsSigner_Available_Bad(t *core.T) {
- subject := &WindowsSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Available()
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestSigntool_WindowsSigner_Available_Ugly(t *core.T) {
- subject := &WindowsSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Available()
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestSigntool_WindowsSigner_Sign_Good(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &WindowsSigner{}
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Sign(ctx, storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestSigntool_WindowsSigner_Sign_Bad(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &WindowsSigner{}
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Sign(ctx, storage.NewMemoryMedium(), "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestSigntool_WindowsSigner_Sign_Ugly(t *core.T) {
- ctx, cancel := core.WithCancel(core.Background())
- cancel()
- subject := &WindowsSigner{}
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = subject.Sign(ctx, storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/templates/release.yml b/pkg/build/templates/release.yml
deleted file mode 100644
index cf2fb23..0000000
--- a/pkg/build/templates/release.yml
+++ /dev/null
@@ -1,990 +0,0 @@
-name: Release
-
-on:
- workflow_call:
- inputs:
- working-directory:
- description: Directory that contains the Core project.
- required: false
- type: string
- default: .
- core-version:
- description: Core CLI version to install.
- required: false
- type: string
- default: latest
- go-version:
- description: Go version to install for Go, Wails, and toolchain-backed builds.
- required: false
- type: string
- default: "1.26"
- node-version:
- description: Node.js version to install for Node and Wails frontend builds.
- required: false
- type: string
- default: "22.x"
- wails-version:
- description: Wails CLI version to install when a Wails project is detected.
- required: false
- type: string
- default: latest
- version:
- description: Release version override.
- required: false
- type: string
- default: ""
- build:
- description: Run the build matrix job.
- required: false
- type: boolean
- default: true
- build-name:
- description: Override the build output name passed to `core build`.
- required: false
- type: string
- default: ""
- build-platform:
- description: Limit the build matrix to a single GOOS/GOARCH target.
- required: false
- type: string
- default: ""
- build-tags:
- description: Comma- or space-separated Go build tags forwarded to `core build`.
- required: false
- type: string
- default: ""
- build-obfuscate:
- description: Enable garble-backed obfuscation for Go and Wails builds.
- required: false
- type: boolean
- default: false
- sign:
- description: Enable platform signing after build.
- required: false
- type: boolean
- default: false
- package:
- description: Upload artifacts and publish the release.
- required: false
- type: boolean
- default: true
- nsis:
- description: Enable NSIS packaging for Windows Wails builds.
- required: false
- type: boolean
- default: false
- deno-build:
- description: Override the Deno frontend build command.
- required: false
- type: string
- default: ""
- npm-build:
- description: Override the npm frontend build command.
- required: false
- type: string
- default: ""
- wails-build-webview2:
- description: Set the WebView2 delivery mode for Windows Wails builds.
- required: false
- type: string
- default: ""
- draft:
- description: Mark the release as a draft.
- required: false
- type: boolean
- default: false
- prerelease:
- description: Mark the release as a pre-release.
- required: false
- type: boolean
- default: false
- archive-format:
- description: Archive compression format for release artefacts.
- required: false
- type: string
- default: ""
- build-cache:
- description: Restore and save build cache directories across workflow runs.
- required: false
- type: boolean
- default: true
- workflow_dispatch:
- inputs:
- working-directory:
- description: Directory that contains the Core project.
- required: false
- type: string
- default: .
- core-version:
- description: Core CLI version to install.
- required: false
- type: string
- default: latest
- go-version:
- description: Go version to install for Go, Wails, and toolchain-backed builds.
- required: false
- type: string
- default: "1.26"
- node-version:
- description: Node.js version to install for Node and Wails frontend builds.
- required: false
- type: string
- default: "22.x"
- wails-version:
- description: Wails CLI version to install when a Wails project is detected.
- required: false
- type: string
- default: latest
- version:
- description: Release version override.
- required: false
- type: string
- default: ""
- build:
- description: Run the build matrix job.
- required: false
- type: boolean
- default: true
- build-name:
- description: Override the build output name passed to `core build`.
- required: false
- type: string
- default: ""
- build-platform:
- description: Limit the build matrix to a single GOOS/GOARCH target.
- required: false
- type: string
- default: ""
- build-tags:
- description: Comma- or space-separated Go build tags forwarded to `core build`.
- required: false
- type: string
- default: ""
- build-obfuscate:
- description: Enable garble-backed obfuscation for Go and Wails builds.
- required: false
- type: boolean
- default: false
- sign:
- description: Enable platform signing after build.
- required: false
- type: boolean
- default: false
- package:
- description: Upload artifacts and publish the release.
- required: false
- type: boolean
- default: true
- nsis:
- description: Enable NSIS packaging for Windows Wails builds.
- required: false
- type: boolean
- default: false
- deno-build:
- description: Override the Deno frontend build command.
- required: false
- type: string
- default: ""
- npm-build:
- description: Override the npm frontend build command.
- required: false
- type: string
- default: ""
- wails-build-webview2:
- description: Set the WebView2 delivery mode for Windows Wails builds.
- required: false
- type: string
- default: ""
- draft:
- description: Mark the release as a draft.
- required: false
- type: boolean
- default: false
- prerelease:
- description: Mark the release as a pre-release.
- required: false
- type: boolean
- default: false
- archive-format:
- description: Archive compression format for release artefacts.
- required: false
- type: string
- default: ""
- build-cache:
- description: Restore and save build cache directories across workflow runs.
- required: false
- type: boolean
- default: true
-
-permissions:
- contents: write
-
-jobs:
- build:
- name: Build ${{ matrix.target }}
- if: ${{ inputs.build && (inputs.build-platform == '' || inputs.build-platform == matrix.target) }}
- runs-on: ${{ matrix.runner }}
- strategy:
- fail-fast: false
- matrix:
- include:
- - target: linux/amd64
- runner: ubuntu-latest
- - target: linux/arm64
- runner: ubuntu-latest
- - target: darwin/amd64
- runner: macos-13
- - target: darwin/arm64
- runner: macos-14
- - target: windows/amd64
- runner: windows-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Discovery
- id: discovery
- working-directory: ${{ inputs.working-directory }}
- shell: bash
- run: |
- set -euo pipefail
-
- truthy_env() {
- case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
- 1|true|yes|on)
- return 0
- ;;
- esac
- return 1
- }
-
- find_visible_files() {
- local maxdepth="$1"
- shift
- find . -maxdepth "$maxdepth" \
- \( -path './.*' -o -path '*/.*' -o -path '*/node_modules' -o -path '*/node_modules/*' \) -prune -o \
- "$@" -print
- }
-
- has_root_package_json=false
- [ -f package.json ] && has_root_package_json=true
-
- has_frontend_package_json=false
- [ -f frontend/package.json ] && has_frontend_package_json=true
-
- has_root_composer_json=false
- [ -f composer.json ] && has_root_composer_json=true
-
- has_root_cargo_toml=false
- [ -f Cargo.toml ] && has_root_cargo_toml=true
-
- has_root_go_mod=false
- [ -f go.mod ] && has_root_go_mod=true
-
- has_root_go_work=false
- [ -f go.work ] && has_root_go_work=true
-
- has_root_main_go=false
- [ -f main.go ] && has_root_main_go=true
-
- has_root_cmakelists=false
- [ -f CMakeLists.txt ] && has_root_cmakelists=true
-
- has_root_wails_json=false
- [ -f wails.json ] && has_root_wails_json=true
-
- has_taskfile=false
- if [ -f Taskfile.yml ] || [ -f Taskfile.yaml ] || [ -f Taskfile ] || [ -f taskfile.yml ] || [ -f taskfile.yaml ]; then
- has_taskfile=true
- fi
-
- configured_build_type=""
- if [ -f .core/build.yaml ]; then
- configured_build_type="$(python - <<'PY'
-from pathlib import Path
-import re
-
-path = Path(".core/build.yaml")
-in_build = False
-
-for raw_line in path.read_text().splitlines():
- line = raw_line.rstrip()
- stripped = line.strip()
- if not stripped or stripped.startswith("#"):
- continue
-
- if not line.startswith((" ", "\t")):
- in_build = stripped == "build:"
- continue
-
- if not in_build:
- continue
-
- match = re.match(r"^\s*type:\s*(.+?)\s*$", line)
- if match:
- print(match.group(1).strip().strip("\"'"), end="")
- break
-PY
-)"
- configured_build_type="$(printf '%s' "$configured_build_type" | tr '[:upper:]' '[:lower:]')"
- fi
-
- has_docs_config=false
- if [ -f mkdocs.yml ] || [ -f mkdocs.yaml ] || [ -f docs/mkdocs.yml ] || [ -f docs/mkdocs.yaml ]; then
- has_docs_config=true
- fi
-
- has_subtree_package_json=false
- if find_visible_files 3 -name package.json \
- | grep -qvE '^\./package\.json$|^\./frontend/package\.json$'; then
- has_subtree_package_json=true
- fi
-
- has_subtree_deno_manifest=false
- if find_visible_files 3 \( -name deno.json -o -name deno.jsonc \) \
- | grep -qvE '^\./deno\.json$|^\./deno\.jsonc$|^\./frontend/deno\.json$|^\./frontend/deno\.jsonc$'; then
- has_subtree_deno_manifest=true
- fi
-
- deno_requested=false
- if truthy_env "${DENO_ENABLE:-}" || [ -n "${DENO_BUILD:-}" ] || [ -n "${{ inputs.deno-build }}" ]; then
- deno_requested=true
- fi
-
- npm_requested=false
- if [ -n "${NPM_BUILD:-}" ] || [ -n "${{ inputs.npm-build }}" ]; then
- npm_requested=true
- fi
-
- has_package_json=false
- if [ "$has_root_package_json" = "true" ] || [ "$has_frontend_package_json" = "true" ] || [ "$has_subtree_package_json" = "true" ]; then
- has_package_json=true
- fi
-
- has_deno_manifest=false
- if [ -f deno.json ] || [ -f deno.jsonc ] || [ -f frontend/deno.json ] || [ -f frontend/deno.jsonc ] || [ "$has_subtree_deno_manifest" = "true" ]; then
- has_deno_manifest=true
- fi
-
- has_frontend=false
- if [ "$has_package_json" = "true" ] || [ "$has_deno_manifest" = "true" ]; then
- has_frontend=true
- fi
-
- has_go_toolchain=false
- if [ "$has_root_go_mod" = "true" ] || [ "$has_root_go_work" = "true" ]; then
- has_go_toolchain=true
- elif find . -maxdepth 4 \
- \( -path './.*' -o -path '*/.*' -o -path '*/node_modules' -o -path '*/node_modules/*' \) -prune -o \
- \( -name go.mod -o -name go.work \) -print \
- | grep -q .; then
- has_go_toolchain=true
- fi
-
- primary_stack_suggestion=unknown
- if [ -n "$configured_build_type" ]; then
- case "$configured_build_type" in
- wails)
- primary_stack_suggestion=wails2
- ;;
- cpp)
- primary_stack_suggestion=cpp
- ;;
- docs)
- primary_stack_suggestion=docs
- ;;
- node)
- primary_stack_suggestion=node
- ;;
- *)
- primary_stack_suggestion="$configured_build_type"
- ;;
- esac
- elif [ "$has_root_wails_json" = "true" ]; then
- primary_stack_suggestion=wails2
- elif { [ "$has_root_go_mod" = "true" ] || [ "$has_root_go_work" = "true" ]; } && [ "$has_frontend" = "true" ]; then
- primary_stack_suggestion=wails2
- elif [ "$has_root_cmakelists" = "true" ]; then
- primary_stack_suggestion=cpp
- elif [ "$has_docs_config" = "true" ] && [ "$has_go_toolchain" != "true" ]; then
- primary_stack_suggestion=docs
- elif [ "$has_frontend" = "true" ] && [ "$has_go_toolchain" != "true" ]; then
- primary_stack_suggestion=node
- elif [ "$has_go_toolchain" = "true" ]; then
- primary_stack_suggestion=go
- elif [ "$has_docs_config" = "true" ]; then
- primary_stack_suggestion=docs
- elif [ "$has_frontend" = "true" ]; then
- primary_stack_suggestion=node
- fi
-
- ref="${GITHUB_REF:-}"
- branch=""
- tag=""
- is_tag=false
- runner_os="${RUNNER_OS:-}"
- runner_arch="${RUNNER_ARCH:-}"
- case "$ref" in
- refs/heads/*)
- branch="${GITHUB_REF_NAME:-${ref#refs/heads/}}"
- ;;
- refs/tags/*)
- tag="${GITHUB_REF_NAME:-${ref#refs/tags/}}"
- is_tag=true
- ;;
- esac
-
- sha="${GITHUB_SHA:-}"
- short_sha=""
- if [ -n "$sha" ]; then
- short_sha="${sha:0:7}"
- fi
-
- repo="${GITHUB_REPOSITORY:-}"
- owner=""
- if [ -n "$repo" ]; then
- owner="${repo%%/*}"
- fi
-
- distro=""
- webkit_package=""
- if [ -f /etc/os-release ]; then
- . /etc/os-release
- if [ "${ID:-}" = "ubuntu" ]; then
- distro="${VERSION_ID:-}"
- webkit_package=libwebkit2gtk-4.0-dev
- if command -v dpkg >/dev/null 2>&1 && dpkg --compare-versions "${VERSION_ID:-0}" ge "24.04"; then
- webkit_package=libwebkit2gtk-4.1-dev
- fi
- fi
- fi
-
- {
- echo "os=$runner_os"
- echo "arch=$runner_arch"
- echo "ref=$ref"
- echo "branch=$branch"
- echo "tag=$tag"
- echo "is_tag=$is_tag"
- echo "sha=$sha"
- echo "short_sha=$short_sha"
- echo "repo=$repo"
- echo "owner=$owner"
- echo "has_root_package_json=$has_root_package_json"
- echo "has_frontend_package_json=$has_frontend_package_json"
- echo "has_root_composer_json=$has_root_composer_json"
- echo "has_root_cargo_toml=$has_root_cargo_toml"
- echo "has_root_go_mod=$has_root_go_mod"
- echo "has_root_go_work=$has_root_go_work"
- echo "has_root_main_go=$has_root_main_go"
- echo "has_root_cmakelists=$has_root_cmakelists"
- echo "has_root_wails_json=$has_root_wails_json"
- echo "has_taskfile=$has_taskfile"
- echo "configured_build_type=$configured_build_type"
- echo "has_package_json=$has_package_json"
- echo "has_deno_manifest=$has_deno_manifest"
- echo "has_subtree_package_json=$has_subtree_package_json"
- echo "has_subtree_deno_manifest=$has_subtree_deno_manifest"
- echo "deno_requested=$deno_requested"
- echo "npm_requested=$npm_requested"
- echo "has_frontend=$has_frontend"
- echo "has_go_toolchain=$has_go_toolchain"
- echo "has_docs_config=$has_docs_config"
- echo "primary_stack_suggestion=$primary_stack_suggestion"
- echo "distro=$distro"
- echo "webkit_package=$webkit_package"
- } >> "${GITHUB_OUTPUT}"
-
- - name: Setup Go
- if: steps.discovery.outputs.has_go_toolchain == 'true' || steps.discovery.outputs.has_taskfile == 'true' || steps.discovery.outputs.configured_build_type == 'go' || steps.discovery.outputs.configured_build_type == 'wails' || steps.discovery.outputs.configured_build_type == 'taskfile'
- uses: actions/setup-go@v5
- with:
- go-version: ${{ inputs.go-version }}
-
- - name: Install Garble
- if: inputs.build-obfuscate
- shell: bash
- run: |
- set -euo pipefail
-
- if ! command -v go >/dev/null 2>&1; then
- echo "Go is not available; skipping Garble installation."
- exit 0
- fi
-
- go install mvdan.cc/garble@latest
-
- gobin="$(go env GOBIN)"
- if [ -z "$gobin" ]; then
- gobin="$(go env GOPATH)/bin"
- fi
- echo "$gobin" >> "${GITHUB_PATH}"
-
- - name: Install Task CLI
- if: steps.discovery.outputs.has_taskfile == 'true' || steps.discovery.outputs.configured_build_type == 'taskfile'
- shell: bash
- run: |
- set -euo pipefail
-
- if command -v task >/dev/null 2>&1; then
- task --version
- exit 0
- fi
-
- go install github.com/go-task/task/v3/cmd/task@latest
-
- gobin="$(go env GOBIN)"
- if [ -z "$gobin" ]; then
- gobin="$(go env GOPATH)/bin"
- fi
- echo "$gobin" >> "${GITHUB_PATH}"
-
- - name: Setup Node
- if: steps.discovery.outputs.has_package_json == 'true' || steps.discovery.outputs.npm_requested == 'true' || steps.discovery.outputs.primary_stack_suggestion == 'wails2' || steps.discovery.outputs.configured_build_type == 'node'
- uses: actions/setup-node@v4
- with:
- node-version: ${{ inputs.node-version }}
-
- - name: Enable Corepack
- if: steps.discovery.outputs.has_package_json == 'true' || steps.discovery.outputs.npm_requested == 'true' || steps.discovery.outputs.primary_stack_suggestion == 'wails2' || steps.discovery.outputs.configured_build_type == 'node'
- shell: bash
- run: |
- set -euo pipefail
- corepack enable
-
- - name: Install frontend dependencies
- if: steps.discovery.outputs.has_package_json == 'true' || steps.discovery.outputs.npm_requested == 'true' || steps.discovery.outputs.primary_stack_suggestion == 'wails2' || steps.discovery.outputs.configured_build_type == 'node'
- working-directory: ${{ inputs.working-directory }}
- shell: bash
- run: |
- set -euo pipefail
-
- find_visible_files() {
- local maxdepth="$1"
- shift
- find . -maxdepth "$maxdepth" \
- \( -path './.*' -o -path '*/.*' -o -path '*/node_modules' -o -path '*/node_modules/*' \) -prune -o \
- "$@" -print
- }
-
- package_manager_from_manifest() {
- local manifest_path="$1/package.json"
- if [ ! -f "$manifest_path" ]; then
- return 0
- fi
-
- node -e '
-const fs = require("fs");
-const manifestPath = process.argv[1];
-try {
- const pkg = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
- const raw = typeof pkg.packageManager === "string" ? pkg.packageManager.trim() : "";
- if (!raw) process.exit(0);
- const manager = raw.split("@")[0];
- if (["bun", "npm", "pnpm", "yarn"].includes(manager)) {
- process.stdout.write(manager);
- }
-} catch (_) {}
-' "$manifest_path"
- }
-
- install_node_package_dir() {
- local dir="$1"
- if [ ! -f "$dir/package.json" ]; then
- return 0
- fi
-
- declared_manager="$(package_manager_from_manifest "$dir")"
- case "$declared_manager" in
- pnpm)
- corepack enable pnpm
- if [ -f "$dir/pnpm-lock.yaml" ]; then
- (cd "$dir" && pnpm install --frozen-lockfile)
- else
- (cd "$dir" && pnpm install)
- fi
- return 0
- ;;
- yarn)
- corepack enable yarn
- if [ -f "$dir/yarn.lock" ]; then
- (cd "$dir" && yarn install --immutable)
- else
- (cd "$dir" && yarn install)
- fi
- return 0
- ;;
- bun)
- if ! command -v bun >/dev/null 2>&1; then
- curl -fsSL https://bun.sh/install | bash
- export PATH="${HOME}/.bun/bin:${PATH}"
- fi
- if [ -f "$dir/bun.lockb" ] || [ -f "$dir/bun.lock" ]; then
- (cd "$dir" && bun install --frozen-lockfile)
- else
- (cd "$dir" && bun install)
- fi
- return 0
- ;;
- npm)
- if [ -f "$dir/package-lock.json" ]; then
- (cd "$dir" && npm ci)
- else
- (cd "$dir" && npm install)
- fi
- return 0
- ;;
- esac
-
- if [ -f "$dir/pnpm-lock.yaml" ]; then
- corepack enable pnpm
- (cd "$dir" && pnpm install --frozen-lockfile)
- return 0
- fi
-
- if [ -f "$dir/yarn.lock" ]; then
- corepack enable yarn
- (cd "$dir" && yarn install --immutable)
- return 0
- fi
-
- if [ -f "$dir/bun.lockb" ] || [ -f "$dir/bun.lock" ]; then
- if ! command -v bun >/dev/null 2>&1; then
- curl -fsSL https://bun.sh/install | bash
- export PATH="${HOME}/.bun/bin:${PATH}"
- fi
- (cd "$dir" && bun install --frozen-lockfile)
- return 0
- fi
-
- if [ -f "$dir/package-lock.json" ]; then
- (cd "$dir" && npm ci)
- return 0
- fi
-
- (cd "$dir" && npm install)
- }
-
- install_node_package_dir "."
-
- if [ -d frontend ]; then
- install_node_package_dir "./frontend"
- fi
-
- while IFS= read -r manifest; do
- dir="$(dirname "$manifest")"
- case "$dir" in
- "."|"./frontend")
- continue
- ;;
- esac
- install_node_package_dir "$dir"
- done < <(find_visible_files 3 -name package.json | sort)
-
- - name: Install Wails CLI
- if: steps.discovery.outputs.primary_stack_suggestion == 'wails2' || steps.discovery.outputs.configured_build_type == 'wails'
- working-directory: ${{ inputs.working-directory }}
- shell: bash
- run: |
- set -euo pipefail
-
- if ! command -v go >/dev/null 2>&1; then
- echo "Go is not available; skipping Wails CLI installation."
- exit 0
- fi
-
- package='github.com/wailsapp/wails/v2/cmd/wails'
- if [ -f go.mod ] && grep -q 'github.com/wailsapp/wails/v3' go.mod; then
- package='github.com/wailsapp/wails/v3/cmd/wails3'
- fi
-
- go install "${package}@${{ inputs.wails-version }}"
- echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}"
-
- - name: Install Core CLI
- uses: dAppCore/build@v3
- with:
- command: build
- working-directory: ${{ inputs.working-directory }}
- core-version: ${{ inputs.core-version }}
-
- - name: Setup Python for Conan and MkDocs
- if: steps.discovery.outputs.has_root_cmakelists == 'true' || steps.discovery.outputs.has_docs_config == 'true' || steps.discovery.outputs.configured_build_type == 'cpp' || steps.discovery.outputs.configured_build_type == 'docs'
- uses: actions/setup-python@v5
- with:
- python-version: '3.x'
-
- - name: Setup PHP and Composer
- if: steps.discovery.outputs.has_root_composer_json == 'true' || steps.discovery.outputs.configured_build_type == 'php'
- shell: bash
- run: |
- set -euo pipefail
-
- if ! command -v php >/dev/null 2>&1; then
- echo "PHP is required to build composer-backed projects on this runner." >&2
- exit 1
- fi
-
- if command -v composer >/dev/null 2>&1; then
- composer --version
- exit 0
- fi
-
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
- php composer-setup.php --install-dir="${RUNNER_TEMP}" --filename=composer
- rm -f composer-setup.php
- echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
-
- - name: Setup Rust
- if: steps.discovery.outputs.has_root_cargo_toml == 'true' || steps.discovery.outputs.configured_build_type == 'rust'
- shell: bash
- run: |
- set -euo pipefail
-
- if command -v cargo >/dev/null 2>&1; then
- cargo --version
- exit 0
- fi
-
- case "${RUNNER_OS}" in
- Linux|macOS)
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
- echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}"
- ;;
- Windows)
- choco install rustup.install -y
- rustup default stable
- echo "${USERPROFILE}\\.cargo\\bin" >> "${GITHUB_PATH}"
- ;;
- *)
- echo "Unsupported runner OS for Rust setup: ${RUNNER_OS}" >&2
- exit 1
- ;;
- esac
-
- - name: Install Conan
- if: steps.discovery.outputs.has_root_cmakelists == 'true' || steps.discovery.outputs.configured_build_type == 'cpp'
- shell: bash
- run: |
- set -euo pipefail
- python -m pip install --upgrade pip
- python -m pip install conan
-
- - name: Install MkDocs
- if: steps.discovery.outputs.has_docs_config == 'true' || steps.discovery.outputs.configured_build_type == 'docs'
- shell: bash
- run: |
- set -euo pipefail
- python -m pip install --upgrade pip
- python -m pip install mkdocs
-
- - name: Setup Deno
- if: steps.discovery.outputs.deno_requested == 'true' || steps.discovery.outputs.has_deno_manifest == 'true'
- uses: denoland/setup-deno@v2
- with:
- deno-version: v2.x
-
- - name: Restore build cache
- if: inputs.build-cache
- uses: actions/cache@v4
- with:
- path: |
- ${{ inputs.working-directory }}/.core/cache
- ${{ inputs.working-directory }}/cache
- key: >-
- core-build-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles(format('{0}/.core/build.yaml', inputs.working-directory), format('{0}/**/go.sum', inputs.working-directory), format('{0}/**/go.work.sum', inputs.working-directory), format('{0}/**/package-lock.json', inputs.working-directory), format('{0}/**/pnpm-lock.yaml', inputs.working-directory), format('{0}/**/yarn.lock', inputs.working-directory), format('{0}/**/bun.lock', inputs.working-directory), format('{0}/**/bun.lockb', inputs.working-directory), format('{0}/**/deno.lock', inputs.working-directory), format('{0}/**/composer.lock', inputs.working-directory), format('{0}/**/poetry.lock', inputs.working-directory), format('{0}/**/requirements.txt', inputs.working-directory), format('{0}/**/Cargo.lock', inputs.working-directory)) }}
- restore-keys: |
- core-build-${{ runner.os }}-${{ matrix.target }}-
- core-build-${{ runner.os }}-
-
- - name: Install Linux Wails dependencies
- if: runner.os == 'Linux' && (steps.discovery.outputs.primary_stack_suggestion == 'wails2' || steps.discovery.outputs.configured_build_type == 'wails')
- working-directory: ${{ inputs.working-directory }}
- shell: bash
- run: |
- set -euo pipefail
-
- webkit_pkg="${{ steps.discovery.outputs.webkit_package }}"
- if [ -z "$webkit_pkg" ]; then
- webkit_pkg=libwebkit2gtk-4.0-dev
- fi
-
- sudo apt-get update
- sudo apt-get install -y "$webkit_pkg"
-
- - name: Build release artefacts
- working-directory: ${{ inputs.working-directory }}
- shell: bash
- run: |
- set -euo pipefail
-
- args=(core build --ci --targets "${{ matrix.target }}" --archive --checksum)
-
- if [ -n "${{ inputs.archive-format }}" ]; then
- args+=(--archive-format "${{ inputs.archive-format }}")
- fi
-
- if [ -n "${{ inputs.build-name }}" ]; then
- args+=(--build-name "${{ inputs.build-name }}")
- fi
-
- if [ -n "${{ inputs.build-tags }}" ]; then
- args+=(--build-tags "${{ inputs.build-tags }}")
- fi
-
- if [ -n "${{ inputs.version }}" ]; then
- args+=(--version "${{ inputs.version }}")
- fi
-
- if [ "${{ inputs.build-obfuscate }}" = "true" ]; then
- args+=(--build-obfuscate)
- fi
-
- if [ "${{ inputs.sign }}" = "true" ]; then
- args+=(--sign=true)
- else
- args+=(--sign=false)
- fi
-
- if [ "${{ inputs.package }}" = "true" ]; then
- args+=(--package)
- else
- args+=(--package=false)
- fi
-
- if [ "${{ inputs.nsis }}" = "true" ]; then
- args+=(--nsis)
- fi
-
- if [ -n "${{ inputs.deno-build }}" ]; then
- args+=(--deno-build "${{ inputs.deno-build }}")
- fi
-
- if [ -n "${{ inputs.npm-build }}" ]; then
- args+=(--npm-build "${{ inputs.npm-build }}")
- fi
-
- if [ -n "${{ inputs.wails-build-webview2 }}" ]; then
- args+=(--wails-build-webview2 "${{ inputs.wails-build-webview2 }}")
- fi
-
- if [ "${{ inputs.build-cache }}" = "true" ]; then
- args+=(--build-cache)
- else
- args+=(--build-cache=false)
- fi
-
- "${args[@]}"
-
- - name: Resolve build name
- id: build_name
- working-directory: ${{ inputs.working-directory }}
- shell: bash
- run: |
- set -euo pipefail
-
- build_name="${{ inputs.build-name }}"
-
- if [ -z "$build_name" ] && [ -f .core/build.yaml ]; then
- build_name="$(python - <<'PY'
-from pathlib import Path
-import re
-
-path = Path(".core/build.yaml")
-in_project = False
-binary = ""
-name = ""
-
-for raw_line in path.read_text().splitlines():
- line = raw_line.rstrip()
- stripped = line.strip()
- if not stripped or stripped.startswith("#"):
- continue
-
- if not line.startswith((" ", "\t")):
- in_project = stripped == "project:"
- continue
-
- if not in_project:
- continue
-
- binary_match = re.match(r"^\s*binary:\s*(.+?)\s*$", line)
- if binary_match and not binary:
- binary = binary_match.group(1).strip().strip("\"'")
-
- name_match = re.match(r"^\s*name:\s*(.+?)\s*$", line)
- if name_match and not name:
- name = name_match.group(1).strip().strip("\"'")
-
-print(binary or name, end="")
-PY
-)"
- fi
-
- if [ -z "$build_name" ]; then
- build_name="${GITHUB_REPOSITORY##*/}"
- fi
-
- echo "value=${build_name}" >> "${GITHUB_OUTPUT}"
-
- - name: Compute artifact upload name
- id: artifact-name
- shell: bash
- run: |
- set -euo pipefail
-
- build_name="${{ steps.build_name.outputs.value }}"
-
- target="${{ matrix.target }}"
- target_os="${target%%/*}"
- target_arch="${target#*/}"
-
- suffix="${{ steps.discovery.outputs.short_sha }}"
- if [ "${{ steps.discovery.outputs.is_tag }}" = "true" ] && [ -n "${{ steps.discovery.outputs.tag }}" ]; then
- suffix="${{ steps.discovery.outputs.tag }}"
- fi
-
- artifact_name="${build_name}_${target_os}_${target_arch}"
- if [ -n "$suffix" ]; then
- artifact_name="${artifact_name}_${suffix}"
- fi
-
- echo "value=${artifact_name}" >> "${GITHUB_OUTPUT}"
-
- - name: Upload artefacts
- if: ${{ inputs.package }}
- uses: actions/upload-artifact@v4
- with:
- name: ${{ steps.artifact-name.outputs.value }}
- path: ${{ inputs.working-directory }}/dist/**
- if-no-files-found: error
-
- release:
- name: Publish release
- if: ${{ inputs.build && inputs.package && startsWith(github.ref, 'refs/tags/') }}
- needs: build
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Download build artefacts
- uses: actions/download-artifact@v4
- with:
- path: ${{ inputs.working-directory }}/dist
- merge-multiple: true
-
- - name: Install Core CLI
- uses: dAppCore/build@v3
- with:
- command: ci
- working-directory: ${{ inputs.working-directory }}
- core-version: ${{ inputs.core-version }}
- version: ${{ inputs.version }}
- draft: ${{ inputs.draft }}
- prerelease: ${{ inputs.prerelease }}
- we-are-go-for-launch: true
diff --git a/pkg/build/testdata/config-project/.core/build.yaml b/pkg/build/testdata/config-project/.core/build.yaml
deleted file mode 100644
index ff3a997..0000000
--- a/pkg/build/testdata/config-project/.core/build.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-# Example build configuration for Core build system
-version: 1
-
-project:
- name: example-cli
- description: An example CLI application
- main: ./cmd/example
- binary: example
-
-build:
- cgo: false
- flags:
- - -trimpath
- ldflags:
- - -s
- - -w
- env: []
-
-targets:
- - os: linux
- arch: amd64
- - os: darwin
- arch: arm64
- - os: windows
- arch: amd64
diff --git a/pkg/build/testdata/cpp-project/CMakeLists.txt b/pkg/build/testdata/cpp-project/CMakeLists.txt
deleted file mode 100644
index f6ba2c7..0000000
--- a/pkg/build/testdata/cpp-project/CMakeLists.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-cmake_minimum_required(VERSION 3.16)
-project(TestCPP)
diff --git a/pkg/build/testdata/docs-project/mkdocs.yml b/pkg/build/testdata/docs-project/mkdocs.yml
deleted file mode 100644
index 0967ef4..0000000
--- a/pkg/build/testdata/docs-project/mkdocs.yml
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/pkg/build/testdata/empty-project/.gitkeep b/pkg/build/testdata/empty-project/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/pkg/build/testdata/go-project/go.mod b/pkg/build/testdata/go-project/go.mod
deleted file mode 100644
index deedf38..0000000
--- a/pkg/build/testdata/go-project/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module example.com/go-project
-
-go 1.21
diff --git a/pkg/build/testdata/monorepo-project/apps/web/package.json b/pkg/build/testdata/monorepo-project/apps/web/package.json
deleted file mode 100644
index 0967ef4..0000000
--- a/pkg/build/testdata/monorepo-project/apps/web/package.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/pkg/build/testdata/multi-project/go.mod b/pkg/build/testdata/multi-project/go.mod
deleted file mode 100644
index f45e24d..0000000
--- a/pkg/build/testdata/multi-project/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module example.com/multi-project
-
-go 1.21
diff --git a/pkg/build/testdata/multi-project/package.json b/pkg/build/testdata/multi-project/package.json
deleted file mode 100644
index 18c5954..0000000
--- a/pkg/build/testdata/multi-project/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "multi-project",
- "version": "1.0.0"
-}
diff --git a/pkg/build/testdata/node-project/package.json b/pkg/build/testdata/node-project/package.json
deleted file mode 100644
index 6d873ce..0000000
--- a/pkg/build/testdata/node-project/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "node-project",
- "version": "1.0.0"
-}
diff --git a/pkg/build/testdata/php-project/composer.json b/pkg/build/testdata/php-project/composer.json
deleted file mode 100644
index 962108e..0000000
--- a/pkg/build/testdata/php-project/composer.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "vendor/php-project",
- "type": "library"
-}
diff --git a/pkg/build/testdata/python-project/pyproject.toml b/pkg/build/testdata/python-project/pyproject.toml
deleted file mode 100644
index 0967ef4..0000000
--- a/pkg/build/testdata/python-project/pyproject.toml
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/pkg/build/testdata/rust-project/Cargo.toml b/pkg/build/testdata/rust-project/Cargo.toml
deleted file mode 100644
index 0967ef4..0000000
--- a/pkg/build/testdata/rust-project/Cargo.toml
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/pkg/build/testdata/wails-project/go.mod b/pkg/build/testdata/wails-project/go.mod
deleted file mode 100644
index e4daed1..0000000
--- a/pkg/build/testdata/wails-project/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module example.com/wails-project
-
-go 1.21
diff --git a/pkg/build/testdata/wails-project/wails.json b/pkg/build/testdata/wails-project/wails.json
deleted file mode 100644
index aaa778f..0000000
--- a/pkg/build/testdata/wails-project/wails.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "wails-project",
- "outputfilename": "wails-project"
-}
diff --git a/pkg/build/version.go b/pkg/build/version.go
deleted file mode 100644
index cea04df..0000000
--- a/pkg/build/version.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package build
-
-import (
- "regexp"
-
- "dappco.re/go"
-)
-
-var safeVersionString = regexp.MustCompile(`^[A-Za-z0-9._+-]+$`)
-
-// ValidateVersionString reports whether a version string is safe to embed in
-// linker flags, generated installers, and release metadata.
-//
-// Safe identifiers are non-empty ASCII strings limited to characters that
-// cannot split a linker flag or shell token.
-func ValidateVersionString(version string) core.Result {
- if !safeVersionString.MatchString(version) {
- return core.Fail(core.E("build.ValidateVersionString", "version must be a non-empty safe release identifier", nil))
- }
-
- return core.Ok(nil)
-}
-
-// ValidateVersionIdentifier reports whether a version override is safe when a
-// caller also permits the absence of a version.
-func ValidateVersionIdentifier(version string) core.Result {
- if version == "" {
- return core.Ok(nil)
- }
- valid := ValidateVersionString(version)
- if !valid.OK {
- return core.Fail(core.E("build.ValidateVersionIdentifier", "version contains unsupported characters", core.NewError(valid.Error())))
- }
-
- return core.Ok(nil)
-}
diff --git a/pkg/build/version_example_test.go b/pkg/build/version_example_test.go
deleted file mode 100644
index fb94a1e..0000000
--- a/pkg/build/version_example_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleValidateVersionString references ValidateVersionString on this package API surface.
-func ExampleValidateVersionString() {
- _ = ValidateVersionString
- core.Println("ValidateVersionString")
- // Output: ValidateVersionString
-}
-
-// ExampleValidateVersionIdentifier references ValidateVersionIdentifier on this package API surface.
-func ExampleValidateVersionIdentifier() {
- _ = ValidateVersionIdentifier
- core.Println("ValidateVersionIdentifier")
- // Output: ValidateVersionIdentifier
-}
diff --git a/pkg/build/version_flags.go b/pkg/build/version_flags.go
deleted file mode 100644
index ed517d8..0000000
--- a/pkg/build/version_flags.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package build
-
-import (
- "dappco.re/go"
-)
-
-// VersionLinkerFlag returns a safe -X linker flag for injecting the build version.
-// Only ASCII version strings without whitespace or shell metacharacters are accepted
-// so the resulting ldflags string cannot be split into extra linker options.
-//
-// flag, err := build.VersionLinkerFlag("v1.2.3")
-func VersionLinkerFlag(version string) core.Result {
- if version == "" {
- return core.Ok("")
- }
- valid := ValidateVersionString(version)
- if !valid.OK {
- return core.Fail(core.E("build.VersionLinkerFlag", "version contains unsupported characters for linker flags", core.NewError(valid.Error())))
- }
-
- return core.Ok(core.Sprintf("-X main.version=%s", version))
-}
diff --git a/pkg/build/version_flags_example_test.go b/pkg/build/version_flags_example_test.go
deleted file mode 100644
index dc04416..0000000
--- a/pkg/build/version_flags_example_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleVersionLinkerFlag references VersionLinkerFlag on this package API surface.
-func ExampleVersionLinkerFlag() {
- _ = VersionLinkerFlag
- core.Println("VersionLinkerFlag")
- // Output: VersionLinkerFlag
-}
diff --git a/pkg/build/version_flags_test.go b/pkg/build/version_flags_test.go
deleted file mode 100644
index 29041ec..0000000
--- a/pkg/build/version_flags_test.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package build
-
-import (
- core "dappco.re/go"
- "testing"
-)
-
-func requireVersionFlag(t *testing.T, result core.Result) string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func requireVersionFlagOK(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requireVersionFlagError(t *testing.T, result core.Result) {
- t.Helper()
- if result.OK {
- t.Fatal("expected error")
- }
-}
-
-func TestVersionLinkerFlag_Good(t *testing.T) {
- flag := requireVersionFlag(t, VersionLinkerFlag("v1.2.3-beta.1+exp.sha"))
- if !stdlibAssertEqual("-X main.version=v1.2.3-beta.1+exp.sha", flag) {
- t.Fatalf("want %v, got %v", "-X main.version=v1.2.3-beta.1+exp.sha", flag)
- }
-}
-
-func TestVersionLinkerFlag_Bad(t *testing.T) {
- result := VersionLinkerFlag("v1.2.3;rm -rf /")
- requireVersionFlagError(t, result)
- if !stdlibAssertContains(result.Error(), "unsupported characters") {
- t.Fatalf("expected %v to contain %v", result.Error(), "unsupported characters")
- }
-}
-
-func TestValidateVersionIdentifier_Bad(t *testing.T) {
- requireVersionFlagOK(t, ValidateVersionIdentifier("v1.2.3"))
- requireVersionFlagOK(t, ValidateVersionIdentifier("dev"))
- requireVersionFlagError(t, ValidateVersionIdentifier("v1.2.3\n--flag"))
-}
-
-func TestVersionFlags_ValidateVersionIdentifier_Good(t *testing.T) {
- t.Run("accepts empty version", func(t *testing.T) {
- requireVersionFlagOK(t, ValidateVersionIdentifier(""))
- })
-
- t.Run("accepts exact safe version", func(t *testing.T) {
- requireVersionFlagOK(t, ValidateVersionIdentifier("v1.2.3-beta.1+exp.sha"))
- })
-}
-
-func TestVersionFlags_ValidateVersionIdentifier_Ugly(t *testing.T) {
- t.Run("rejects non-ASCII identifiers", func(t *testing.T) {
- requireVersionFlagError(t, ValidateVersionIdentifier("v1.2.3-β"))
- })
-
- t.Run("rejects shell metacharacters", func(t *testing.T) {
- requireVersionFlagError(t, ValidateVersionIdentifier("v1.2.3 && echo unsafe"))
- })
-
- t.Run("rejects surrounding whitespace", func(t *testing.T) {
- requireVersionFlagError(t, ValidateVersionIdentifier(" v1.2.3-beta.1+exp.sha "))
- })
-}
-
-func TestVersionFlags_VersionLinkerFlag_Good(t *testing.T) {
- t.Run("renders exact safe version", func(t *testing.T) {
- flag := requireVersionFlag(t, VersionLinkerFlag("v1.2.3"))
- if !stdlibAssertEqual("-X main.version=v1.2.3", flag) {
- t.Fatalf("want %v, got %v", "-X main.version=v1.2.3", flag)
- }
- })
-}
-
-func TestVersionFlags_VersionLinkerFlag_Ugly(t *testing.T) {
- t.Run("empty version is a no-op", func(t *testing.T) {
- flag := requireVersionFlag(t, VersionLinkerFlag(""))
- if !stdlibAssertEmpty(flag) {
- t.Fatalf("expected empty, got %v", flag)
- }
- })
-
- t.Run("rejects surrounding whitespace", func(t *testing.T) {
- requireVersionFlagError(t, VersionLinkerFlag(" v1.2.3 "))
- })
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestVersionFlags_VersionLinkerFlag_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = VersionLinkerFlag("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
diff --git a/pkg/build/version_templates.go b/pkg/build/version_templates.go
deleted file mode 100644
index 46a3884..0000000
--- a/pkg/build/version_templates.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package build
-
-import "dappco.re/go"
-
-// ExpandVersionTemplate resolves the RFC-documented version placeholders used
-// across build and release config surfaces.
-//
-// Supported placeholders:
-// - {{.Tag}} / {{Tag}} → v-prefixed version/tag
-// - {{.Version}} / {{Version}} → legacy full version value
-//
-// The helper also understands v{{.Version}} / v{{Version}} so RFC examples
-// that prefix the placeholder do not render a duplicated "v".
-func ExpandVersionTemplate(value, version string) string {
- if value == "" || version == "" {
- return value
- }
-
- trimmedVersion := core.TrimPrefix(version, "v")
-
- value = core.Replace(value, "v{{.Version}}", "v"+trimmedVersion)
- value = core.Replace(value, "v{{Version}}", "v"+trimmedVersion)
- value = core.Replace(value, "{{.Tag}}", version)
- value = core.Replace(value, "{{Tag}}", version)
- value = core.Replace(value, "{{.Version}}", version)
- value = core.Replace(value, "{{Version}}", version)
-
- return value
-}
-
-// ExpandVersionTemplates resolves version placeholders across a string slice.
-func ExpandVersionTemplates(values []string, version string) []string {
- if len(values) == 0 || version == "" {
- return values
- }
-
- expanded := make([]string, 0, len(values))
- for _, value := range values {
- expanded = append(expanded, ExpandVersionTemplate(value, version))
- }
-
- return expanded
-}
-
-// ExpandVersionTemplateMap resolves version placeholders across a string map.
-func ExpandVersionTemplateMap(values map[string]string, version string) map[string]string {
- if len(values) == 0 || version == "" {
- return CloneStringMap(values)
- }
-
- expanded := make(map[string]string, len(values))
- for key, value := range values {
- expanded[key] = ExpandVersionTemplate(value, version)
- }
-
- return expanded
-}
diff --git a/pkg/build/version_templates_example_test.go b/pkg/build/version_templates_example_test.go
deleted file mode 100644
index e2e07e5..0000000
--- a/pkg/build/version_templates_example_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleExpandVersionTemplate references ExpandVersionTemplate on this package API surface.
-func ExampleExpandVersionTemplate() {
- _ = ExpandVersionTemplate
- core.Println("ExpandVersionTemplate")
- // Output: ExpandVersionTemplate
-}
-
-// ExampleExpandVersionTemplates references ExpandVersionTemplates on this package API surface.
-func ExampleExpandVersionTemplates() {
- _ = ExpandVersionTemplates
- core.Println("ExpandVersionTemplates")
- // Output: ExpandVersionTemplates
-}
-
-// ExampleExpandVersionTemplateMap references ExpandVersionTemplateMap on this package API surface.
-func ExampleExpandVersionTemplateMap() {
- _ = ExpandVersionTemplateMap
- core.Println("ExpandVersionTemplateMap")
- // Output: ExpandVersionTemplateMap
-}
diff --git a/pkg/build/version_templates_test.go b/pkg/build/version_templates_test.go
deleted file mode 100644
index 4001f04..0000000
--- a/pkg/build/version_templates_test.go
+++ /dev/null
@@ -1,122 +0,0 @@
-package build
-
-import (
- core "dappco.re/go"
- "testing"
-)
-
-func TestBuild_ExpandVersionTemplate_Good(t *testing.T) {
- t.Run("expands tag placeholders", func(t *testing.T) {
- value := ExpandVersionTemplate("-X main.Version={{.Tag}}", "v1.2.3")
- if !stdlibAssertEqual("-X main.Version=v1.2.3", value) {
- t.Fatalf("want %v, got %v", "-X main.Version=v1.2.3", value)
- }
-
- })
-
- t.Run("avoids duplicated v prefix in version placeholders", func(t *testing.T) {
- value := ExpandVersionTemplate("v{{.Version}}", "v1.2.3")
- if !stdlibAssertEqual("v1.2.3", value) {
- t.Fatalf("want %v, got %v", "v1.2.3", value)
- }
-
- })
-
- t.Run("preserves legacy full version expansion", func(t *testing.T) {
- value := ExpandVersionTemplate("release-{{.Version}}", "v1.2.3")
- if !stdlibAssertEqual("release-v1.2.3", value) {
- t.Fatalf("want %v, got %v", "release-v1.2.3", value)
- }
-
- })
-
- t.Run("supports shorthand placeholders", func(t *testing.T) {
- value := ExpandVersionTemplate("{{Tag}}-{{Version}}", "v1.2.3")
- if !stdlibAssertEqual("v1.2.3-v1.2.3", value) {
- t.Fatalf("want %v, got %v", "v1.2.3-v1.2.3", value)
- }
-
- })
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestVersionTemplates_ExpandVersionTemplate_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ExpandVersionTemplate("agent", "v1.2.3")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestVersionTemplates_ExpandVersionTemplate_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ExpandVersionTemplate("", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestVersionTemplates_ExpandVersionTemplate_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ExpandVersionTemplate("agent", "v1.2.3")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestVersionTemplates_ExpandVersionTemplates_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ExpandVersionTemplates([]string{"agent"}, "v1.2.3")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestVersionTemplates_ExpandVersionTemplates_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ExpandVersionTemplates([]string{"agent"}, "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestVersionTemplates_ExpandVersionTemplates_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ExpandVersionTemplates([]string{"agent"}, "v1.2.3")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestVersionTemplates_ExpandVersionTemplateMap_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ExpandVersionTemplateMap(nil, "v1.2.3")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestVersionTemplates_ExpandVersionTemplateMap_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ExpandVersionTemplateMap(nil, "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestVersionTemplates_ExpandVersionTemplateMap_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ExpandVersionTemplateMap(nil, "v1.2.3")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/version_test.go b/pkg/build/version_test.go
deleted file mode 100644
index a42afa2..0000000
--- a/pkg/build/version_test.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package build
-
-import (
- core "dappco.re/go"
- "testing"
-)
-
-func TestValidateVersionString_Good(t *testing.T) {
- for _, version := range []string{
- "v1.2.3",
- "1.2.3-beta.1+exp.sha_5114f85",
- "dev-build_20260425",
- } {
- t.Run(version, func(t *testing.T) {
- requireVersionFlagOK(t, ValidateVersionString(version))
- })
- }
-}
-
-func TestValidateVersionString_Bad(t *testing.T) {
- for _, version := range []string{
- "v1.2.3;rm",
- `v1.2.3"`,
- "v1.2.3$IFS",
- "v1.2.3`uname`",
- } {
- t.Run(version, func(t *testing.T) {
- requireVersionFlagError(t, ValidateVersionString(version))
- })
- }
-}
-
-func TestValidateVersionString_Ugly(t *testing.T) {
- for _, version := range []string{
- "",
- " ",
- " v1.2.3",
- "v1.2.3 ",
- "v1.2.3 beta",
- } {
- t.Run(version, func(t *testing.T) {
- requireVersionFlagError(t, ValidateVersionString(version))
- })
- }
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestVersion_ValidateVersionString_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ValidateVersionString("v1.2.3")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestVersion_ValidateVersionString_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ValidateVersionString("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestVersion_ValidateVersionString_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ValidateVersionString("v1.2.3")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestVersion_ValidateVersionIdentifier_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ValidateVersionIdentifier("v1.2.3")
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestVersion_ValidateVersionIdentifier_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ValidateVersionIdentifier("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestVersion_ValidateVersionIdentifier_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ValidateVersionIdentifier("v1.2.3")
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/workflow.go b/pkg/build/workflow.go
deleted file mode 100644
index 1b46883..0000000
--- a/pkg/build/workflow.go
+++ /dev/null
@@ -1,529 +0,0 @@
-// Package build provides project type detection and cross-compilation for the Core build system.
-// This file exposes the release workflow generator and its path-resolution helpers.
-package build
-
-import (
- "embed"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- io_interface "dappco.re/go/build/pkg/storage"
-)
-
-//go:embed templates/release.yml
-var releaseWorkflowTemplate embed.FS
-
-// DefaultReleaseWorkflowPath is the conventional output path for the release workflow.
-//
-// path := build.DefaultReleaseWorkflowPath // ".github/workflows/release.yml"
-const DefaultReleaseWorkflowPath = ".github/workflows/release.yml"
-
-// DefaultReleaseWorkflowFileName is the workflow filename used when a directory-style
-// output path is supplied.
-const DefaultReleaseWorkflowFileName = "release.yml"
-
-// WriteReleaseWorkflow writes the embedded release workflow template to outputPath.
-//
-// build.WriteReleaseWorkflow(io.Local, "") // writes .github/workflows/release.yml
-// build.WriteReleaseWorkflow(io.Local, "ci") // writes ./ci/release.yml under the project root
-// build.WriteReleaseWorkflow(io.Local, "./ci") // writes ./ci/release.yml under the project root
-// build.WriteReleaseWorkflow(io.Local, ".github/workflows") // writes .github/workflows/release.yml
-// build.WriteReleaseWorkflow(io.Local, "ci/release.yml") // writes ./ci/release.yml under the project root
-// build.WriteReleaseWorkflow(io.Local, "/tmp/repo/.github/workflows/release.yml") // writes the absolute path unchanged
-func WriteReleaseWorkflow(filesystem io_interface.Medium, outputPath string) core.Result {
- if filesystem == nil {
- return core.Fail(core.E("build.WriteReleaseWorkflow", "filesystem medium is required", nil))
- }
-
- outputPath = cleanWorkflowInput(outputPath)
- if outputPath == "" {
- outputPath = DefaultReleaseWorkflowPath
- }
-
- if isWorkflowDirectoryInput(outputPath) || filesystem.IsDir(outputPath) {
- outputPath = ax.Join(outputPath, DefaultReleaseWorkflowFileName)
- }
-
- content, err := releaseWorkflowTemplate.ReadFile("templates/release.yml")
- if err != nil {
- return core.Fail(core.E("build.WriteReleaseWorkflow", "failed to read embedded workflow template", err))
- }
-
- created := filesystem.EnsureDir(ax.Dir(outputPath))
- if !created.OK {
- return core.Fail(core.E("build.WriteReleaseWorkflow", "failed to create release workflow directory", core.NewError(created.Error())))
- }
-
- written := filesystem.Write(outputPath, string(content))
- if !written.OK {
- return core.Fail(core.E("build.WriteReleaseWorkflow", "failed to write release workflow", core.NewError(written.Error())))
- }
-
- return core.Ok(nil)
-}
-
-// ReleaseWorkflowPath joins a project directory with the conventional workflow path.
-//
-// build.ReleaseWorkflowPath("/home/user/project") // /home/user/project/.github/workflows/release.yml
-func ReleaseWorkflowPath(projectDir string) string {
- return ax.Join(projectDir, DefaultReleaseWorkflowPath)
-}
-
-// ResolveReleaseWorkflowOutputPathWithMedium resolves the workflow output path
-// relative to the project directory and treats an existing directory as a
-// workflow directory even when the caller omits a trailing slash.
-//
-// build.ResolveReleaseWorkflowOutputPathWithMedium(io.Local, "/tmp/project", "ci") // /tmp/project/ci/release.yml when /tmp/project/ci exists
-// build.ResolveReleaseWorkflowOutputPathWithMedium(io.Local, "/tmp/project", ".github/workflows") // /tmp/project/.github/workflows/release.yml
-func ResolveReleaseWorkflowOutputPathWithMedium(filesystem io_interface.Medium, projectDir, outputPath string) string {
- outputPath = cleanWorkflowInput(outputPath)
- if outputPath == "" {
- return ReleaseWorkflowPath(projectDir)
- }
-
- resolved := ResolveReleaseWorkflowPath(projectDir, outputPath)
- if filesystem != nil && filesystem.IsDir(resolved) {
- return ax.Join(resolved, DefaultReleaseWorkflowFileName)
- }
-
- return resolved
-}
-
-// ResolveReleaseWorkflowPath resolves the workflow output path relative to the
-// project directory when the caller supplies a relative path.
-//
-// build.ResolveReleaseWorkflowPath("/tmp/project", "") // /tmp/project/.github/workflows/release.yml
-// build.ResolveReleaseWorkflowPath("/tmp/project", "./ci") // /tmp/project/ci/release.yml
-// build.ResolveReleaseWorkflowPath("/tmp/project", ".github/workflows") // /tmp/project/.github/workflows/release.yml
-// build.ResolveReleaseWorkflowPath("/tmp/project", "ci/release.yml") // /tmp/project/ci/release.yml
-// build.ResolveReleaseWorkflowPath("/tmp/project", "ci") // /tmp/project/ci/release.yml
-// build.ResolveReleaseWorkflowPath("/tmp/project", "/tmp/release.yml") // /tmp/release.yml
-func ResolveReleaseWorkflowPath(projectDir, outputPath string) string {
- outputPath = cleanWorkflowInput(outputPath)
- if outputPath == "" {
- return ReleaseWorkflowPath(projectDir)
- }
- if isWorkflowDirectoryPath(outputPath) || isWorkflowDirectoryInput(outputPath) {
- if ax.IsAbs(outputPath) {
- return ax.Join(outputPath, DefaultReleaseWorkflowFileName)
- }
- return ax.Join(projectDir, outputPath, DefaultReleaseWorkflowFileName)
- }
- if !ax.IsAbs(outputPath) {
- return ax.Join(projectDir, outputPath)
- }
- return outputPath
-}
-
-// ResolveReleaseWorkflowInputPath resolves a workflow target from the CLI/API
-// `path` field and its `output` alias.
-//
-// build.ResolveReleaseWorkflowInputPath("/tmp/project", "", "") // /tmp/project/.github/workflows/release.yml
-// build.ResolveReleaseWorkflowInputPath("/tmp/project", "./ci", "") // /tmp/project/ci/release.yml
-// build.ResolveReleaseWorkflowInputPath("/tmp/project", "ci/release.yml", "") // /tmp/project/ci/release.yml
-// build.ResolveReleaseWorkflowInputPath("/tmp/project", "", "ci/release.yml") // /tmp/project/ci/release.yml
-// build.ResolveReleaseWorkflowInputPath("/tmp/project", "ci/release.yml", "ci.yml") // error
-func ResolveReleaseWorkflowInputPath(projectDir, pathInput, outputPathInput string) core.Result {
- return resolveReleaseWorkflowInputPathPair(
- pathInput,
- outputPathInput,
- func(input string) string {
- return resolveReleaseWorkflowInputPath(projectDir, input, nil)
- },
- "build.ResolveReleaseWorkflowInputPath",
- )
-}
-
-// ResolveReleaseWorkflowInputPathWithMedium resolves the workflow path and
-// treats an existing directory as a directory even when the caller omits a
-// trailing slash.
-//
-// build.ResolveReleaseWorkflowInputPathWithMedium(io.Local, "/tmp/project", "ci", "") // /tmp/project/ci/release.yml when /tmp/project/ci exists
-// build.ResolveReleaseWorkflowInputPathWithMedium(io.Local, "/tmp/project", "./ci", "") // /tmp/project/ci/release.yml
-func ResolveReleaseWorkflowInputPathWithMedium(filesystem io_interface.Medium, projectDir, pathInput, outputPathInput string) core.Result {
- return resolveReleaseWorkflowInputPathPair(
- pathInput,
- outputPathInput,
- func(input string) string {
- return resolveReleaseWorkflowInputPath(projectDir, input, filesystem)
- },
- "build.ResolveReleaseWorkflowInputPathWithMedium",
- )
-}
-
-// ResolveReleaseWorkflowInputPathAliases resolves the workflow path across the
-// public path aliases and treats an existing directory as a directory even
-// when the caller omits a trailing slash.
-//
-// build.ResolveReleaseWorkflowInputPathAliases(io.Local, "/tmp/project", "ci", "", "", "") // /tmp/project/ci/release.yml
-// build.ResolveReleaseWorkflowInputPathAliases(io.Local, "/tmp/project", "", "ci", "", "") // /tmp/project/ci/release.yml
-// build.ResolveReleaseWorkflowInputPathAliases(io.Local, "/tmp/project", "", "", "ci", "") // /tmp/project/ci/release.yml
-// build.ResolveReleaseWorkflowInputPathAliases(io.Local, "/tmp/project", "", "", "", "ci") // /tmp/project/ci/release.yml
-func ResolveReleaseWorkflowInputPathAliases(filesystem io_interface.Medium, projectDir, pathInput, workflowPathInput, workflowPathSnakeInput, workflowPathHyphenInput string) core.Result {
- return resolveReleaseWorkflowInputPathAliasSet(
- filesystem,
- projectDir,
- releaseWorkflowPathAlias,
- pathInput,
- workflowPathInput,
- workflowPathSnakeInput,
- workflowPathHyphenInput,
- "build.ResolveReleaseWorkflowInputPathAliases",
- )
-}
-
-const releaseWorkflowPathAlias = "pa" + "th"
-
-// ResolveReleaseWorkflowOutputPath("ci/release.yml", "", "") // "ci/release.yml"
-// ResolveReleaseWorkflowOutputPath("", "ci/release.yml", "") // "ci/release.yml"
-// ResolveReleaseWorkflowOutputPath("", "", "ci/release.yml") // "ci/release.yml"
-// ResolveReleaseWorkflowOutputPath("ci/release.yml", "ops.yml", "") // error
-func ResolveReleaseWorkflowOutputPath(outputPathInput, outputPathSnakeInput, legacyOutputInput string) core.Result {
- return ResolveReleaseWorkflowOutputPathAliases(
- outputPathInput,
- "",
- outputPathSnakeInput,
- legacyOutputInput,
- "",
- "",
- "",
- "",
- "",
- )
-}
-
-// ResolveReleaseWorkflowOutputPathAliases resolves every public workflow output
-// alias across the CLI, API, and UI layers.
-//
-// build.ResolveReleaseWorkflowOutputPathAliases("ci/release.yml", "", "", "", "", "", "", "", "") // "ci/release.yml"
-// build.ResolveReleaseWorkflowOutputPathAliases("", "ci/release.yml", "", "", "", "", "", "", "") // "ci/release.yml"
-// build.ResolveReleaseWorkflowOutputPathAliases("", "", "", "", "ci/release.yml", "", "", "", "") // "ci/release.yml"
-// build.ResolveReleaseWorkflowOutputPathAliases("", "", "", "", "", "ci/release.yml", "", "", "") // "ci/release.yml"
-func ResolveReleaseWorkflowOutputPathAliases(
- outputPathInput,
- outputPathHyphenInput,
- outputPathSnakeInput,
- legacyOutputInput,
- workflowOutputPathInput,
- workflowOutputSnakeInput,
- workflowOutputHyphenInput,
- workflowOutputPathSnakeInput,
- workflowOutputPathHyphenInput string,
-) core.Result {
- return resolveReleaseWorkflowOutputAliasSet(
- outputPathInput,
- outputPathHyphenInput,
- outputPathSnakeInput,
- legacyOutputInput,
- workflowOutputPathInput,
- workflowOutputSnakeInput,
- workflowOutputHyphenInput,
- workflowOutputPathSnakeInput,
- workflowOutputPathHyphenInput,
- "build.ResolveReleaseWorkflowOutputPathAliases",
- )
-}
-
-// ResolveReleaseWorkflowOutputPathAliasesInProject resolves the workflow output
-// aliases relative to a project directory before checking for conflicts.
-//
-// build.ResolveReleaseWorkflowOutputPathAliasesInProject("/tmp/project", "ci/release.yml", "", "", "", "", "", "", "") // "/tmp/project/ci/release.yml"
-// build.ResolveReleaseWorkflowOutputPathAliasesInProject("/tmp/project", "", "", "", "", "/tmp/project/ci/release.yml", "", "", "") // "/tmp/project/ci/release.yml"
-func ResolveReleaseWorkflowOutputPathAliasesInProject(
- projectDir,
- outputPathInput,
- outputPathHyphenInput,
- outputPathSnakeInput,
- legacyOutputInput,
- workflowOutputPathInput,
- workflowOutputSnakeInput,
- workflowOutputHyphenInput,
- workflowOutputPathSnakeInput,
- workflowOutputPathHyphenInput string,
-) core.Result {
- return ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(
- nil,
- projectDir,
- outputPathInput,
- outputPathHyphenInput,
- outputPathSnakeInput,
- legacyOutputInput,
- workflowOutputPathInput,
- workflowOutputSnakeInput,
- workflowOutputHyphenInput,
- workflowOutputPathSnakeInput,
- workflowOutputPathHyphenInput,
- )
-}
-
-// ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium resolves the
-// workflow output aliases relative to a project directory and uses the
-// provided filesystem medium to treat existing directories as workflow
-// directories even when callers omit a trailing separator.
-//
-// build.ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(io.Local, "/tmp/project", "", "", "", "", "/tmp/project/ci", "", "", "") // "/tmp/project/ci/release.yml"
-func ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(
- filesystem io_interface.Medium,
- projectDir,
- outputPathInput,
- outputPathHyphenInput,
- outputPathSnakeInput,
- legacyOutputInput,
- workflowOutputPathInput,
- workflowOutputSnakeInput,
- workflowOutputHyphenInput,
- workflowOutputPathSnakeInput,
- workflowOutputPathHyphenInput string,
-) core.Result {
- return resolveReleaseWorkflowOutputAliasSetInProject(
- filesystem,
- projectDir,
- outputPathInput,
- outputPathHyphenInput,
- outputPathSnakeInput,
- legacyOutputInput,
- workflowOutputPathInput,
- workflowOutputSnakeInput,
- workflowOutputHyphenInput,
- workflowOutputPathSnakeInput,
- workflowOutputPathHyphenInput,
- "build.ResolveReleaseWorkflowOutputPathAliasesInProject",
- )
-}
-
-// resolveReleaseWorkflowInputPathPair resolves the workflow path from the path
-// and output aliases, rejecting conflicting values and preferring explicit
-// inputs over the default.
-func resolveReleaseWorkflowInputPathPair(pathInput, outputPathInput string, resolve func(string) string, errorName string) core.Result {
- pathInput = cleanWorkflowInput(pathInput)
- outputPathInput = cleanWorkflowInput(outputPathInput)
-
- if pathInput != "" && outputPathInput != "" {
- resolvedPath := resolve(pathInput)
- resolvedOutput := resolve(outputPathInput)
- if resolvedPath != resolvedOutput {
- return core.Fail(core.E(errorName, "path and output specify different locations", nil))
- }
- return core.Ok(resolvedPath)
- }
-
- if pathInput != "" {
- return core.Ok(resolve(pathInput))
- }
-
- if outputPathInput != "" {
- return core.Ok(resolve(outputPathInput))
- }
-
- return core.Ok(resolve(""))
-}
-
-// resolveReleaseWorkflowOutputAliasSet resolves a workflow output alias set by
-// trimming whitespace, rejecting conflicts, and returning the first non-empty
-// value when aliases agree.
-func resolveReleaseWorkflowOutputAliasSet(
- outputPathInput,
- outputPathHyphenInput,
- outputPathSnakeInput,
- legacyOutputInput,
- workflowOutputPathInput,
- workflowOutputSnakeInput,
- workflowOutputHyphenInput,
- workflowOutputPathSnakeInput,
- workflowOutputPathHyphenInput,
- errorName string,
-) core.Result {
- values := []string{
- normalizeWorkflowOutputAlias(outputPathInput),
- normalizeWorkflowOutputAlias(outputPathHyphenInput),
- normalizeWorkflowOutputAlias(outputPathSnakeInput),
- normalizeWorkflowOutputAlias(legacyOutputInput),
- normalizeWorkflowOutputAlias(workflowOutputPathInput),
- normalizeWorkflowOutputAlias(workflowOutputSnakeInput),
- normalizeWorkflowOutputAlias(workflowOutputHyphenInput),
- normalizeWorkflowOutputAlias(workflowOutputPathSnakeInput),
- normalizeWorkflowOutputAlias(workflowOutputPathHyphenInput),
- }
-
- var resolved string
- for _, value := range values {
- if value == "" {
- continue
- }
- if resolved == "" {
- resolved = value
- continue
- }
- if resolved != value {
- return core.Fail(core.E(errorName, "output aliases specify different locations", nil))
- }
- }
-
- return core.Ok(resolved)
-}
-
-// resolveReleaseWorkflowOutputAliasSetInProject resolves workflow output aliases
-// against a project directory so relative and absolute paths can be compared.
-func resolveReleaseWorkflowOutputAliasSetInProject(
- filesystem io_interface.Medium,
- projectDir,
- outputPathInput,
- outputPathHyphenInput,
- outputPathSnakeInput,
- legacyOutputInput,
- workflowOutputPathInput,
- workflowOutputSnakeInput,
- workflowOutputHyphenInput,
- workflowOutputPathSnakeInput,
- workflowOutputPathHyphenInput,
- errorName string,
-) core.Result {
- values := []string{
- cleanWorkflowInput(outputPathInput),
- cleanWorkflowInput(outputPathHyphenInput),
- cleanWorkflowInput(outputPathSnakeInput),
- cleanWorkflowInput(legacyOutputInput),
- cleanWorkflowInput(workflowOutputPathInput),
- cleanWorkflowInput(workflowOutputSnakeInput),
- cleanWorkflowInput(workflowOutputHyphenInput),
- cleanWorkflowInput(workflowOutputPathSnakeInput),
- cleanWorkflowInput(workflowOutputPathHyphenInput),
- }
-
- var resolved string
- for _, value := range values {
- if value == "" {
- continue
- }
-
- candidate := ResolveReleaseWorkflowOutputPathWithMedium(filesystem, projectDir, value)
- if resolved == "" {
- resolved = candidate
- continue
- }
-
- if resolved != candidate {
- return core.Fail(core.E(errorName, "output aliases specify different locations", nil))
- }
- }
-
- return core.Ok(resolved)
-}
-
-// normalizeWorkflowOutputAlias canonicalises a workflow output alias for comparison.
-func normalizeWorkflowOutputAlias(path string) string {
- path = cleanWorkflowInput(path)
- if path == "" {
- return ""
- }
-
- return ax.Clean(path)
-}
-
-// resolveReleaseWorkflowInputPath resolves one workflow input into a file path.
-//
-// resolveReleaseWorkflowInputPath("/tmp/project", "ci", io.Local) // /tmp/project/ci/release.yml
-func resolveReleaseWorkflowInputPath(projectDir, input string, medium io_interface.Medium) string {
- input = cleanWorkflowInput(input)
- if input == "" {
- return ReleaseWorkflowPath(projectDir)
- }
-
- if isWorkflowDirectoryInput(input) {
- if ax.IsAbs(input) {
- return ax.Join(input, DefaultReleaseWorkflowFileName)
- }
- return ax.Join(projectDir, input, DefaultReleaseWorkflowFileName)
- }
-
- resolved := ResolveReleaseWorkflowPath(projectDir, input)
- if medium != nil && medium.IsDir(resolved) {
- return ax.Join(resolved, DefaultReleaseWorkflowFileName)
- }
- return resolved
-}
-
-// resolveReleaseWorkflowInputPathAliasSet resolves a workflow path from a set
-// of aliases and rejects conflicting values.
-func resolveReleaseWorkflowInputPathAliasSet(filesystem io_interface.Medium, projectDir, fieldLabel, primaryInput, secondaryInput, tertiaryInput, quaternaryInput, errorName string) core.Result {
- values := []string{
- cleanWorkflowInput(primaryInput),
- cleanWorkflowInput(secondaryInput),
- cleanWorkflowInput(tertiaryInput),
- cleanWorkflowInput(quaternaryInput),
- }
-
- var resolved string
- for _, value := range values {
- if value == "" {
- continue
- }
-
- candidate := resolveReleaseWorkflowInputPath(projectDir, value, filesystem)
- if resolved == "" {
- resolved = candidate
- continue
- }
-
- if resolved != candidate {
- return core.Fail(core.E(errorName, fieldLabel+" aliases specify different locations", nil))
- }
- }
-
- return core.Ok(resolved)
-}
-
-// isWorkflowDirectoryPath reports whether a workflow path is explicitly marked
-// as a directory with a trailing separator.
-func isWorkflowDirectoryPath(path string) bool {
- path = cleanWorkflowInput(path)
- if path == "" {
- return false
- }
-
- if path == "." || path == "./" || path == ".\\" {
- return true
- }
-
- last := path[len(path)-1]
- return last == '/' || last == '\\'
-}
-
-// isWorkflowDirectoryInput reports whether a workflow input should be treated
-// as a directory target. This includes explicit directory paths and bare names
-// without path separators or a file extension, plus current-directory-prefixed
-// directory targets like "./ci" and the conventional ".github/workflows" path.
-func isWorkflowDirectoryInput(path string) bool {
- path = cleanWorkflowInput(path)
- if isWorkflowDirectoryPath(path) {
- return true
- }
- if path == "" || ax.Ext(path) != "" {
- return false
- }
- if !core.Contains(path, "/") && !core.Contains(path, "\\") {
- return true
- }
-
- if ax.Base(path) == "workflows" {
- return true
- }
-
- if core.HasPrefix(path, "./") || core.HasPrefix(path, ".\\") {
- trimmed := core.TrimPrefix(core.TrimPrefix(path, "./"), ".\\")
- if trimmed == "" {
- return false
- }
- if ax.Base(trimmed) == "workflows" {
- return true
- }
- return !core.Contains(trimmed, "/") && !core.Contains(trimmed, "\\")
- }
-
- return false
-}
-
-// cleanWorkflowInput trims surrounding whitespace from a workflow path input.
-func cleanWorkflowInput(path string) string {
- return core.Trim(path)
-}
diff --git a/pkg/build/workflow_example_test.go b/pkg/build/workflow_example_test.go
deleted file mode 100644
index bf1b211..0000000
--- a/pkg/build/workflow_example_test.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleWriteReleaseWorkflow references WriteReleaseWorkflow on this package API surface.
-func ExampleWriteReleaseWorkflow() {
- _ = WriteReleaseWorkflow
- core.Println("WriteReleaseWorkflow")
- // Output: WriteReleaseWorkflow
-}
-
-// ExampleReleaseWorkflowPath references ReleaseWorkflowPath on this package API surface.
-func ExampleReleaseWorkflowPath() {
- _ = ReleaseWorkflowPath
- core.Println("ReleaseWorkflowPath")
- // Output: ReleaseWorkflowPath
-}
-
-// ExampleResolveReleaseWorkflowOutputPathWithMedium references ResolveReleaseWorkflowOutputPathWithMedium on this package API surface.
-func ExampleResolveReleaseWorkflowOutputPathWithMedium() {
- _ = ResolveReleaseWorkflowOutputPathWithMedium
- core.Println("ResolveReleaseWorkflowOutputPathWithMedium")
- // Output: ResolveReleaseWorkflowOutputPathWithMedium
-}
-
-// ExampleResolveReleaseWorkflowPath references ResolveReleaseWorkflowPath on this package API surface.
-func ExampleResolveReleaseWorkflowPath() {
- _ = ResolveReleaseWorkflowPath
- core.Println("ResolveReleaseWorkflowPath")
- // Output: ResolveReleaseWorkflowPath
-}
-
-// ExampleResolveReleaseWorkflowInputPath references ResolveReleaseWorkflowInputPath on this package API surface.
-func ExampleResolveReleaseWorkflowInputPath() {
- _ = ResolveReleaseWorkflowInputPath
- core.Println("ResolveReleaseWorkflowInputPath")
- // Output: ResolveReleaseWorkflowInputPath
-}
-
-// ExampleResolveReleaseWorkflowInputPathWithMedium references ResolveReleaseWorkflowInputPathWithMedium on this package API surface.
-func ExampleResolveReleaseWorkflowInputPathWithMedium() {
- _ = ResolveReleaseWorkflowInputPathWithMedium
- core.Println("ResolveReleaseWorkflowInputPathWithMedium")
- // Output: ResolveReleaseWorkflowInputPathWithMedium
-}
-
-// ExampleResolveReleaseWorkflowInputPathAliases references ResolveReleaseWorkflowInputPathAliases on this package API surface.
-func ExampleResolveReleaseWorkflowInputPathAliases() {
- _ = ResolveReleaseWorkflowInputPathAliases
- core.Println("ResolveReleaseWorkflowInputPathAliases")
- // Output: ResolveReleaseWorkflowInputPathAliases
-}
-
-// ExampleResolveReleaseWorkflowOutputPath references ResolveReleaseWorkflowOutputPath on this package API surface.
-func ExampleResolveReleaseWorkflowOutputPath() {
- _ = ResolveReleaseWorkflowOutputPath
- core.Println("ResolveReleaseWorkflowOutputPath")
- // Output: ResolveReleaseWorkflowOutputPath
-}
-
-// ExampleResolveReleaseWorkflowOutputPathAliases references ResolveReleaseWorkflowOutputPathAliases on this package API surface.
-func ExampleResolveReleaseWorkflowOutputPathAliases() {
- _ = ResolveReleaseWorkflowOutputPathAliases
- core.Println("ResolveReleaseWorkflowOutputPathAliases")
- // Output: ResolveReleaseWorkflowOutputPathAliases
-}
-
-// ExampleResolveReleaseWorkflowOutputPathAliasesInProject references ResolveReleaseWorkflowOutputPathAliasesInProject on this package API surface.
-func ExampleResolveReleaseWorkflowOutputPathAliasesInProject() {
- _ = ResolveReleaseWorkflowOutputPathAliasesInProject
- core.Println("ResolveReleaseWorkflowOutputPathAliasesInProject")
- // Output: ResolveReleaseWorkflowOutputPathAliasesInProject
-}
-
-// ExampleResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium references ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium on this package API surface.
-func ExampleResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium() {
- _ = ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium
- core.Println("ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium")
- // Output: ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium
-}
diff --git a/pkg/build/workflow_test.go b/pkg/build/workflow_test.go
deleted file mode 100644
index 593f1ef..0000000
--- a/pkg/build/workflow_test.go
+++ /dev/null
@@ -1,835 +0,0 @@
-package build
-
-import (
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/internal/buildtest"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func requireWorkflowOK(t *testing.T, result core.Result) {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
-}
-
-func requireWorkflowString(t *testing.T, result core.Result) string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func requireWorkflowError(t *testing.T, result core.Result) string {
- t.Helper()
- if result.OK {
- t.Fatal("expected error")
- }
- return result.Error()
-}
-
-func TestWorkflow_WriteReleaseWorkflow_Good(t *testing.T) {
- t.Run("writes the embedded template to the default path", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- requireWorkflowOK(t, WriteReleaseWorkflow(fs, ""))
-
- content := requireWorkflowString(t, fs.Read(DefaultReleaseWorkflowPath))
-
- template, err := releaseWorkflowTemplate.ReadFile("templates/release.yml")
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if !stdlibAssertEqual(string(template), content) {
- t.Fatalf("want %v, got %v", string(template), content)
- }
- buildtest.AssertReleaseWorkflowContent(t, content)
-
- })
-
- t.Run("writes to a custom path", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- requireWorkflowOK(t, WriteReleaseWorkflow(fs, "custom/workflow.yml"))
-
- content := requireWorkflowString(t, fs.Read("custom/workflow.yml"))
- if stdlibAssertEmpty(content) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("trims surrounding whitespace from the output path", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- requireWorkflowOK(t, WriteReleaseWorkflow(fs, " ci "))
-
- content := requireWorkflowString(t, fs.Read("ci/release.yml"))
- if stdlibAssertEmpty(content) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("writes release.yml for a bare directory-style path", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- requireWorkflowOK(t, WriteReleaseWorkflow(fs, "ci"))
-
- content := requireWorkflowString(t, fs.Read("ci/release.yml"))
- if stdlibAssertEmpty(content) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("writes release.yml inside an existing directory", func(t *testing.T) {
- projectDir := t.TempDir()
- outputDir := ax.Join(projectDir, "ci")
- requireWorkflowOK(t, ax.MkdirAll(outputDir, 0o755))
-
- requireWorkflowOK(t, WriteReleaseWorkflow(storage.Local, outputDir))
-
- content := requireWorkflowString(t, storage.Local.Read(ax.Join(outputDir, DefaultReleaseWorkflowFileName)))
-
- template, err := releaseWorkflowTemplate.ReadFile("templates/release.yml")
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if !stdlibAssertEqual(string(template), content) {
- t.Fatalf("want %v, got %v", string(template), content)
- }
-
- })
-
- t.Run("writes release.yml for directory-style output paths", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- requireWorkflowOK(t, WriteReleaseWorkflow(fs, "ci/"))
-
- content := requireWorkflowString(t, fs.Read("ci/release.yml"))
- if stdlibAssertEmpty(content) {
- t.Fatal("expected non-empty")
- }
-
- })
-
- t.Run("creates parent directories on a real filesystem", func(t *testing.T) {
- projectDir := t.TempDir()
- path := ax.Join(projectDir, ".github", "workflows", "release.yml")
-
- requireWorkflowOK(t, WriteReleaseWorkflow(storage.Local, path))
-
- content := requireWorkflowString(t, storage.Local.Read(path))
-
- template, err := releaseWorkflowTemplate.ReadFile("templates/release.yml")
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if !stdlibAssertEqual(string(template), content) {
- t.Fatalf("want %v, got %v", string(template), content)
- }
-
- })
-}
-
-func TestWorkflow_WriteReleaseWorkflow_Bad(t *testing.T) {
- t.Run("rejects a nil filesystem medium", func(t *testing.T) {
- err := requireWorkflowError(t, WriteReleaseWorkflow(nil, ""))
- if !stdlibAssertContains(err, "filesystem medium is required") {
- t.Fatalf("expected %v to contain %v", err, "filesystem medium is required")
- }
-
- })
-}
-
-func TestWorkflow_ReleaseWorkflowPath_Good(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/.github/workflows/release.yml", ReleaseWorkflowPath("/tmp/project")) {
- t.Fatalf("want %v, got %v", "/tmp/project/.github/workflows/release.yml", ReleaseWorkflowPath("/tmp/project"))
- }
-
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathWithMedium_Good(t *testing.T) {
- t.Run("treats an existing directory as a workflow directory", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireWorkflowOK(t, fs.EnsureDir("/tmp/project/ci"))
-
- path := ResolveReleaseWorkflowOutputPathWithMedium(fs, "/tmp/project", "ci")
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("keeps explicit file paths unchanged", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := ResolveReleaseWorkflowOutputPathWithMedium(fs, "/tmp/project", "ci/release.yml")
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-}
-
-func TestWorkflow_ResolveReleaseWorkflowPath_Good(t *testing.T) {
- t.Run("uses the conventional path when empty", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/.github/workflows/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "")) {
- t.Fatalf("want %v, got %v", "/tmp/project/.github/workflows/release.yml", ResolveReleaseWorkflowPath("/tmp/project", ""))
- }
-
- })
-
- t.Run("joins relative paths to the project directory", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "ci/release.yml")) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "ci/release.yml"))
- }
-
- })
-
- t.Run("treats bare relative directory names as directories", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "ci")) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "ci"))
- }
-
- })
-
- t.Run("treats current-directory-prefixed directory names as directories", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "./ci")) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "./ci"))
- }
-
- })
-
- t.Run("treats the conventional workflows directory as a directory", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/.github/workflows/release.yml", ResolveReleaseWorkflowPath("/tmp/project", ".github/workflows")) {
- t.Fatalf("want %v, got %v", "/tmp/project/.github/workflows/release.yml", ResolveReleaseWorkflowPath("/tmp/project", ".github/workflows"))
- }
-
- })
-
- t.Run("treats current-directory-prefixed workflows directories as directories", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/.github/workflows/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "./.github/workflows")) {
- t.Fatalf("want %v, got %v", "/tmp/project/.github/workflows/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "./.github/workflows"))
- }
-
- })
-
- t.Run("keeps nested extensionless paths as files", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/ci/release", ResolveReleaseWorkflowPath("/tmp/project", "ci/release")) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release", ResolveReleaseWorkflowPath("/tmp/project", "ci/release"))
- }
-
- })
-
- t.Run("treats the current directory as a workflow directory", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/release.yml", ResolveReleaseWorkflowPath("/tmp/project", ".")) {
- t.Fatalf("want %v, got %v", "/tmp/project/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "."))
- }
-
- })
-
- t.Run("treats trailing-slash relative paths as directories", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "ci/")) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "ci/"))
- }
-
- })
-
- t.Run("keeps absolute paths unchanged", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "/tmp/release.yml")) {
- t.Fatalf("want %v, got %v", "/tmp/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "/tmp/release.yml"))
- }
-
- })
-
- t.Run("treats trailing-slash absolute paths as directories", func(t *testing.T) {
- if !stdlibAssertEqual("/tmp/workflows/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "/tmp/workflows/")) {
- t.Fatalf("want %v, got %v", "/tmp/workflows/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "/tmp/workflows/"))
- }
-
- })
-}
-
-func TestWorkflow_ResolveReleaseWorkflowInputPath_Good(t *testing.T) {
- t.Run("uses the conventional path when both inputs are empty", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", "", ""))
- if !stdlibAssertEqual("/tmp/project/.github/workflows/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/.github/workflows/release.yml", path)
- }
-
- })
-
- t.Run("accepts path as the primary input", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", "ci/release.yml", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts bare directory-style path as the primary input", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", "ci", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts current-directory-prefixed directory-style path as the primary input", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", "./ci", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the conventional workflows directory as the primary input", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", ".github/workflows", ""))
- if !stdlibAssertEqual("/tmp/project/.github/workflows/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/.github/workflows/release.yml", path)
- }
-
- })
-
- t.Run("accepts current-directory-prefixed workflows directories as the primary input", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", "./.github/workflows", ""))
- if !stdlibAssertEqual("/tmp/project/.github/workflows/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/.github/workflows/release.yml", path)
- }
-
- })
-
- t.Run("keeps nested extensionless paths as files", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", "ci/release", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release", path)
- }
-
- })
-
- t.Run("accepts the current directory as the primary input", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", ".", ""))
- if !stdlibAssertEqual("/tmp/project/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/release.yml", path)
- }
-
- })
-
- t.Run("accepts output as an alias for path", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", "", "ci/release.yml"))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("trims surrounding whitespace from inputs", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", " ci ", " "))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts matching path and output values", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", "ci/release.yml", "ci/release.yml"))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts matching directory-style path and output values", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPath("/tmp/project", "ci/", "ci/"))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-}
-
-func TestWorkflow_ResolveReleaseWorkflowInputPath_Bad(t *testing.T) {
- t.Run("rejects conflicting path and output values", func(t *testing.T) {
- err := requireWorkflowError(t, ResolveReleaseWorkflowInputPath("/tmp/project", "ci/release.yml", "ops/release.yml"))
- if !stdlibAssertContains(err, "path and output specify different locations") {
- t.Fatalf("expected %v to contain %v", err, "path and output specify different locations")
- }
-
- })
-}
-
-func TestWorkflow_ResolveReleaseWorkflowInputPathWithMedium_Good(t *testing.T) {
- t.Run("treats an existing directory as a workflow directory", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireWorkflowOK(t, fs.EnsureDir("/tmp/project/ci"))
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathWithMedium(fs, "/tmp/project", "ci", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("treats a bare directory-style path as a workflow directory", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathWithMedium(fs, "/tmp/project", "ci", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("treats a current-directory-prefixed directory-style path as a workflow directory", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathWithMedium(fs, "/tmp/project", "./ci", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("treats the conventional workflows directory as a workflow directory", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathWithMedium(fs, "/tmp/project", ".github/workflows", ""))
- if !stdlibAssertEqual("/tmp/project/.github/workflows/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/.github/workflows/release.yml", path)
- }
-
- })
-
- t.Run("treats current-directory-prefixed workflows directories as workflow directories", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathWithMedium(fs, "/tmp/project", "./.github/workflows", ""))
- if !stdlibAssertEqual("/tmp/project/.github/workflows/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/.github/workflows/release.yml", path)
- }
-
- })
-
- t.Run("keeps a file path unchanged when the target is not a directory", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathWithMedium(fs, "/tmp/project", "ci/release.yml", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("normalizes matching directory aliases", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireWorkflowOK(t, fs.EnsureDir("/tmp/project/ci"))
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathWithMedium(fs, "/tmp/project", "ci", "ci/"))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("trims surrounding whitespace before resolving", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireWorkflowOK(t, fs.EnsureDir("/tmp/project/ci"))
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathWithMedium(fs, "/tmp/project", " ci ", " "))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-}
-
-func TestWorkflow_ResolveReleaseWorkflowInputPathAliases_Good(t *testing.T) {
- t.Run("accepts the preferred path input", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathAliases(fs, "/tmp/project", "ci", "", "", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the workflowPath alias", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathAliases(fs, "/tmp/project", "", "ci", "", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the workflow_path alias", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathAliases(fs, "/tmp/project", "", "", "ci", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the workflow-path alias", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathAliases(fs, "/tmp/project", "", "", "", "ci"))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-
- t.Run("normalises matching aliases", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireWorkflowOK(t, fs.EnsureDir("/tmp/project/ci"))
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowInputPathAliases(fs, "/tmp/project", "ci/", "./ci", "ci", ""))
- if !stdlibAssertEqual("/tmp/project/ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "/tmp/project/ci/release.yml", path)
- }
-
- })
-}
-
-func TestWorkflow_ResolveReleaseWorkflowInputPathAliases_Bad(t *testing.T) {
- fs := storage.NewMemoryMedium()
-
- err := requireWorkflowError(t, ResolveReleaseWorkflowInputPathAliases(fs, "/tmp/project", "ci/release.yml", "ops/release.yml", "", ""))
- if !stdlibAssertContains(err, "path aliases specify different locations") {
- t.Fatalf("expected %v to contain %v", err, "path aliases specify different locations")
- }
-
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPath_Good(t *testing.T) {
- t.Run("accepts the preferred output path", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPath("ci/release.yml", "", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the snake_case output path alias", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPath("", "ci/release.yml", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the hyphenated output path alias", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliases("", "ci/release.yml", "", "", "", "", "", "", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the legacy output alias", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPath("", "", "ci/release.yml"))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("trims surrounding whitespace from aliases", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPath(" ci/release.yml ", " ", " "))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts matching aliases", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPath("ci/release.yml", "ci/release.yml", "ci/release.yml"))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("normalises equivalent path aliases", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPath("ci/release.yml", "./ci/release.yml", "ci/release.yml"))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPath_Bad(t *testing.T) {
- err := requireWorkflowError(t, ResolveReleaseWorkflowOutputPath("ci/release.yml", "ops/release.yml", ""))
- if !stdlibAssertContains(err, "output aliases specify different locations") {
- t.Fatalf("expected %v to contain %v", err, "output aliases specify different locations")
- }
-
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathAliases_Good(t *testing.T) {
- t.Run("accepts workflowOutputPath aliases", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliases("", "", "", "", "ci/release.yml", "", "", "", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the hyphenated workflowOutputPath alias", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliases("", "", "", "", "", "", "ci/release.yml", "", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the workflow_output alias", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliases("", "", "", "", "", "ci/release.yml", "", "", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("accepts the workflow-output alias", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliases("", "", "", "", "", "", "ci/release.yml", "", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-
- t.Run("normalises matching workflow output aliases", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliases("ci/release.yml", "", "", "./ci/release.yml", "ci/release.yml", "", "", "", ""))
- if !stdlibAssertEqual("ci/release.yml", path) {
- t.Fatalf("want %v, got %v", "ci/release.yml", path)
- }
-
- })
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathAliasesInProject_Good(t *testing.T) {
- projectDir := t.TempDir()
- absolutePath := ax.Join(projectDir, "ci", "release.yml")
- absoluteDirectory := ax.Join(projectDir, "ops")
- requireWorkflowOK(t, ax.MkdirAll(absoluteDirectory, 0o755))
-
- t.Run("accepts the preferred output path", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliasesInProject(projectDir, "ci/release.yml", "", "", "", "", "", "", "", ""))
- if !stdlibAssertEqual(absolutePath, path) {
- t.Fatalf("want %v, got %v", absolutePath, path)
- }
-
- })
-
- t.Run("accepts an absolute workflow output alias equivalent to the project path", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliasesInProject(projectDir, "", "", "", "", absolutePath, "", "", "", ""))
- if !stdlibAssertEqual(absolutePath, path) {
- t.Fatalf("want %v, got %v", absolutePath, path)
- }
-
- })
-
- t.Run("accepts matching relative and absolute aliases", func(t *testing.T) {
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliasesInProject(projectDir, "ci/release.yml", "", "", "", "", "", "", "", absolutePath))
- if !stdlibAssertEqual(absolutePath, path) {
- t.Fatalf("want %v, got %v", absolutePath, path)
- }
-
- })
-
- t.Run("treats an existing absolute directory as a workflow directory", func(t *testing.T) {
- fs := storage.NewMemoryMedium()
- requireWorkflowOK(t, fs.EnsureDir(absoluteDirectory))
-
- path := requireWorkflowString(t, ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(fs, projectDir, "", "", "", "", absoluteDirectory, "", "", "", ""))
- if !stdlibAssertEqual(ax.Join(absoluteDirectory, DefaultReleaseWorkflowFileName), path) {
- t.Fatalf("want %v, got %v", ax.Join(absoluteDirectory, DefaultReleaseWorkflowFileName), path)
- }
-
- })
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathAliasesInProject_Bad(t *testing.T) {
- projectDir := t.TempDir()
-
- err := requireWorkflowError(t, ResolveReleaseWorkflowOutputPathAliasesInProject(projectDir, "ci/release.yml", "", "", "", "", "", "", "", ax.Join(projectDir, "ops", "release.yml")))
- if !stdlibAssertContains(err, "output aliases specify different locations") {
- t.Fatalf("expected %v to contain %v", err, "output aliases specify different locations")
- }
-
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathAliases_Bad(t *testing.T) {
- err := requireWorkflowError(t, ResolveReleaseWorkflowOutputPathAliases("ci/release.yml", "", "", "", "ops/release.yml", "", "", "", ""))
- if !stdlibAssertContains(err, "output aliases specify different locations") {
- t.Fatalf("expected %v to contain %v", err, "output aliases specify different locations")
- }
-
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestWorkflow_WriteReleaseWorkflow_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteReleaseWorkflow(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ReleaseWorkflowPath_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ReleaseWorkflowPath("")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWorkflow_ReleaseWorkflowPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ReleaseWorkflowPath(core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathWithMedium_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowOutputPathWithMedium(storage.NewMemoryMedium(), "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathWithMedium_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowOutputPathWithMedium(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowPath_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowPath("", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowPath(core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowInputPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowInputPath(core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowInputPathWithMedium_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowInputPathWithMedium(storage.NewMemoryMedium(), "", "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowInputPathWithMedium_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowInputPathWithMedium(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowInputPathAliases_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowInputPathAliases(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPath_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowOutputPath(core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathAliases_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowOutputPathAliases(core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathAliasesInProject_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowOutputPathAliasesInProject(core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium_Good(t *core.T) {
- goodCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- goodCalls++
- })
- core.AssertEqual(t, 1, goodCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(storage.NewMemoryMedium(), "", "", "", "", "", "", "", "", "", "")
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestWorkflow_ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"), core.Path(t.TempDir(), "go-build-compliance"))
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/build/xcode_cloud.go b/pkg/build/xcode_cloud.go
deleted file mode 100644
index c5d9ae5..0000000
--- a/pkg/build/xcode_cloud.go
+++ /dev/null
@@ -1,357 +0,0 @@
-package build
-
-import (
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-const (
- // XcodeCloudScriptsDir is the repository-relative directory used by Xcode Cloud.
- XcodeCloudScriptsDir = "ci_scripts"
-
- // XcodeCloudPostCloneScriptName installs toolchains and project dependencies.
- XcodeCloudPostCloneScriptName = "ci_post_clone.sh"
- // XcodeCloudPreXcodebuildScriptName runs the Apple pipeline before xcodebuild.
- XcodeCloudPreXcodebuildScriptName = "ci_pre_xcodebuild.sh"
- // XcodeCloudPostXcodebuildScriptName verifies the built bundle after xcodebuild.
- XcodeCloudPostXcodebuildScriptName = "ci_post_xcodebuild.sh"
-)
-
-// HasXcodeCloudConfig reports whether apple.xcode_cloud contains workflow metadata.
-func HasXcodeCloudConfig(cfg *BuildConfig) bool {
- if cfg == nil {
- return false
- }
-
- if core.Trim(cfg.Apple.XcodeCloud.Workflow) != "" {
- return true
- }
-
- return len(cfg.Apple.XcodeCloud.Triggers) > 0
-}
-
-// GenerateXcodeCloudScripts renders the three Xcode Cloud helper scripts.
-func GenerateXcodeCloudScripts(projectDir string, cfg *BuildConfig) map[string]string {
- if cfg == nil {
- cfg = DefaultConfig()
- }
-
- bundleName := resolveXcodeCloudBundleName(projectDir, cfg)
- buildCommand := resolveXcodeCloudBuildCommand(cfg)
-
- return map[string]string{
- XcodeCloudPostCloneScriptName: generateXcodeCloudPostCloneScript(),
- XcodeCloudPreXcodebuildScriptName: generateXcodeCloudPreXcodebuildScript(buildCommand),
- XcodeCloudPostXcodebuildScriptName: generateXcodeCloudPostXcodebuildScript(
- bundleName,
- ),
- }
-}
-
-// WriteXcodeCloudScripts writes the Xcode Cloud helper scripts to ci_scripts/.
-func WriteXcodeCloudScripts(filesystem storage.Medium, projectDir string, cfg *BuildConfig) core.Result {
- if filesystem == nil {
- return core.Fail(core.E("build.WriteXcodeCloudScripts", "filesystem medium is required", nil))
- }
-
- scripts := GenerateXcodeCloudScripts(projectDir, cfg)
- orderedNames := []string{
- XcodeCloudPostCloneScriptName,
- XcodeCloudPreXcodebuildScriptName,
- XcodeCloudPostXcodebuildScriptName,
- }
-
- baseDir := ax.Join(projectDir, XcodeCloudScriptsDir)
- created := filesystem.EnsureDir(baseDir)
- if !created.OK {
- return core.Fail(core.E("build.WriteXcodeCloudScripts", "failed to create Xcode Cloud scripts directory", core.NewError(created.Error())))
- }
-
- paths := make([]string, 0, len(orderedNames))
- for _, name := range orderedNames {
- path := ax.Join(baseDir, name)
- written := filesystem.WriteMode(path, scripts[name], 0o755)
- if !written.OK {
- return core.Fail(core.E("build.WriteXcodeCloudScripts", "failed to write "+name, core.NewError(written.Error())))
- }
- paths = append(paths, path)
- }
-
- return core.Ok(paths)
-}
-
-func resolveXcodeCloudBundleName(projectDir string, cfg *BuildConfig) string {
- if cfg != nil {
- if cfg.Project.Binary != "" {
- return cfg.Project.Binary
- }
- if cfg.Project.Name != "" {
- return cfg.Project.Name
- }
- }
-
- if core.Trim(projectDir) == "" {
- return "App"
- }
-
- return ax.Base(projectDir)
-}
-
-func resolveXcodeCloudBuildCommand(cfg *BuildConfig) string {
- options := DefaultAppleOptions()
- if cfg != nil {
- options = cfg.Apple.Resolve()
- }
-
- args := []string{
- "core",
- "build",
- "apple",
- "--arch",
- shellQuote(firstNonEmpty(options.Arch, defaultAppleArch)),
- "--config",
- shellQuote(ax.Join(ConfigDir, ConfigFileName)),
- }
-
- if !options.Sign {
- args = append(args, "--sign=false")
- }
- if !options.Notarise {
- args = append(args, "--notarise=false")
- }
- if options.DMG {
- args = append(args, "--dmg")
- }
- if options.TestFlight {
- args = append(args, "--testflight")
- }
- if options.AppStore {
- args = append(args, "--appstore")
- }
- if core.Trim(options.BundleID) != "" {
- args = append(args, "--bundle-id", shellQuote(options.BundleID))
- }
- if core.Trim(options.TeamID) != "" {
- args = append(args, "--team-id", shellQuote(options.TeamID))
- }
-
- return core.Join(" ", args...)
-}
-
-func generateXcodeCloudPostCloneScript() string {
- return core.Trim(`#!/usr/bin/env bash
-set -euo pipefail
-
-export PATH="${HOME}/go/bin:${HOME}/.deno/bin:${HOME}/.bun/bin:${PATH}"
-
-deno_requested() {
- case "${DENO_ENABLE:-}" in
- 1|true|TRUE|yes|YES|on|ON)
- return 0
- ;;
- esac
-
- [ -n "${DENO_BUILD:-}" ]
-}
-
-find_visible_files() {
- local maxdepth="$1"
- shift
- find . -maxdepth "$maxdepth" \
- \( -path './.*' -o -path '*/.*' -o -path '*/node_modules' -o -path '*/node_modules/*' \) -prune -o \
- "$@" -print
-}
-
-package_manager_from_manifest() {
- local manifest_path="$1/package.json"
- if [ ! -f "$manifest_path" ]; then
- return 0
- fi
-
- node -e '
-const fs = require("fs");
-const manifestPath = process.argv[1];
-try {
- const pkg = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
- const raw = typeof pkg.packageManager === "string" ? pkg.packageManager.trim() : "";
- if (!raw) process.exit(0);
- const manager = raw.split("@")[0];
- if (["bun", "npm", "pnpm", "yarn"].includes(manager)) {
- process.stdout.write(manager);
- }
-} catch (_) {}
-' "$manifest_path"
-}
-
-install_node_package_dir() {
- local dir="$1"
- if [ ! -f "$dir/package.json" ]; then
- return 0
- fi
-
- declared_manager="$(package_manager_from_manifest "$dir")"
- case "$declared_manager" in
- pnpm)
- corepack enable pnpm
- if [ -f "$dir/pnpm-lock.yaml" ]; then
- (cd "$dir" && pnpm install --frozen-lockfile)
- else
- (cd "$dir" && pnpm install)
- fi
- return 0
- ;;
- yarn)
- corepack enable yarn
- if [ -f "$dir/yarn.lock" ]; then
- (cd "$dir" && yarn install --immutable)
- else
- (cd "$dir" && yarn install)
- fi
- return 0
- ;;
- bun)
- if ! command -v bun >/dev/null 2>&1; then
- curl -fsSL https://bun.sh/install | bash
- export PATH="${HOME}/.bun/bin:${PATH}"
- fi
- if [ -f "$dir/bun.lockb" ] || [ -f "$dir/bun.lock" ]; then
- (cd "$dir" && bun install --frozen-lockfile)
- else
- (cd "$dir" && bun install)
- fi
- return 0
- ;;
- npm)
- if [ -f "$dir/package-lock.json" ]; then
- (cd "$dir" && npm ci)
- else
- (cd "$dir" && npm install)
- fi
- return 0
- ;;
- esac
-
- if [ -f "$dir/pnpm-lock.yaml" ]; then
- corepack enable pnpm
- (cd "$dir" && pnpm install --frozen-lockfile)
- return 0
- fi
-
- if [ -f "$dir/yarn.lock" ]; then
- corepack enable yarn
- (cd "$dir" && yarn install --immutable)
- return 0
- fi
-
- if [ -f "$dir/bun.lockb" ] || [ -f "$dir/bun.lock" ]; then
- if ! command -v bun >/dev/null 2>&1; then
- curl -fsSL https://bun.sh/install | bash
- export PATH="${HOME}/.bun/bin:${PATH}"
- fi
- (cd "$dir" && bun install --frozen-lockfile)
- return 0
- fi
-
- if [ -f "$dir/package-lock.json" ]; then
- (cd "$dir" && npm ci)
- return 0
- fi
-
- (cd "$dir" && npm install)
-}
-
-if ! command -v go >/dev/null 2>&1; then
- if command -v brew >/dev/null 2>&1; then
- brew install go
- else
- echo "Go is required for Xcode Cloud builds." >&2
- exit 1
- fi
-fi
-
-if ! command -v node >/dev/null 2>&1; then
- if command -v brew >/dev/null 2>&1; then
- brew install node
- else
- echo "Node.js is required for Xcode Cloud builds." >&2
- exit 1
- fi
-fi
-
-if ! command -v wails3 >/dev/null 2>&1 && ! command -v wails >/dev/null 2>&1; then
- go install github.com/wailsapp/wails/v3/cmd/wails3@latest
-fi
-
-if deno_requested || find_visible_files 3 \( -name deno.json -o -name deno.jsonc \) | grep -q .; then
- if ! command -v deno >/dev/null 2>&1; then
- curl -fsSL https://deno.land/install.sh | sh
- export PATH="${HOME}/.deno/bin:${PATH}"
- fi
-fi
-
-install_node_package_dir "."
-
-if [ -d frontend ]; then
- install_node_package_dir "./frontend"
-fi
-
-while IFS= read -r manifest; do
- dir="$(dirname "$manifest")"
- case "$dir" in
- "."|"./frontend")
- continue
- ;;
- esac
- install_node_package_dir "$dir"
-done < <(find_visible_files 3 -name package.json | sort)
-`) + "\n"
-}
-
-func generateXcodeCloudPreXcodebuildScript(buildCommand string) string {
- return core.Trim(core.Sprintf(`#!/usr/bin/env bash
-set -euo pipefail
-
-export PATH="${HOME}/go/bin:${HOME}/.deno/bin:${HOME}/.bun/bin:${PATH}"
-
-%s
-`, buildCommand)) + "\n"
-}
-
-func generateXcodeCloudPostXcodebuildScript(bundleName string) string {
- bundlePath := ax.Join("dist", "apple", bundleName+".app")
- executablePath := ax.Join(bundlePath, "Contents", "MacOS", bundleName)
-
- return core.Trim(core.Sprintf(`#!/usr/bin/env bash
-set -euo pipefail
-
-BUNDLE_PATH=%s
-EXECUTABLE_PATH=%s
-
-if [ ! -d "$BUNDLE_PATH" ]; then
- echo "Expected bundle not found: $BUNDLE_PATH" >&2
- exit 1
-fi
-
-if [ ! -x "$EXECUTABLE_PATH" ]; then
- echo "Expected executable not found: $EXECUTABLE_PATH" >&2
- exit 1
-fi
-
-if command -v codesign >/dev/null 2>&1; then
- codesign --verify --deep --strict "$BUNDLE_PATH"
-fi
-
-if command -v spctl >/dev/null 2>&1; then
- spctl --assess --type execute "$BUNDLE_PATH" || true
-fi
-`, shellQuote(bundlePath), shellQuote(executablePath))) + "\n"
-}
-
-func shellQuote(value string) string {
- if value == "" {
- return "''"
- }
-
- return "'" + core.Replace(value, "'", `'"'"'`) + "'"
-}
diff --git a/pkg/build/xcode_cloud_example_test.go b/pkg/build/xcode_cloud_example_test.go
deleted file mode 100644
index 3f190d4..0000000
--- a/pkg/build/xcode_cloud_example_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package build
-
-import core "dappco.re/go"
-
-// ExampleHasXcodeCloudConfig references HasXcodeCloudConfig on this package API surface.
-func ExampleHasXcodeCloudConfig() {
- _ = HasXcodeCloudConfig
- core.Println("HasXcodeCloudConfig")
- // Output: HasXcodeCloudConfig
-}
-
-// ExampleGenerateXcodeCloudScripts references GenerateXcodeCloudScripts on this package API surface.
-func ExampleGenerateXcodeCloudScripts() {
- _ = GenerateXcodeCloudScripts
- core.Println("GenerateXcodeCloudScripts")
- // Output: GenerateXcodeCloudScripts
-}
-
-// ExampleWriteXcodeCloudScripts references WriteXcodeCloudScripts on this package API surface.
-func ExampleWriteXcodeCloudScripts() {
- _ = WriteXcodeCloudScripts
- core.Println("WriteXcodeCloudScripts")
- // Output: WriteXcodeCloudScripts
-}
diff --git a/pkg/build/xcode_cloud_test.go b/pkg/build/xcode_cloud_test.go
deleted file mode 100644
index 3c543b1..0000000
--- a/pkg/build/xcode_cloud_test.go
+++ /dev/null
@@ -1,265 +0,0 @@
-package build
-
-import (
- "testing"
-
- core "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- storage "dappco.re/go/build/pkg/storage"
-)
-
-func requireXcodeCloudPaths(t *testing.T, result core.Result) []string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.([]string)
-}
-
-func requireXcodeCloudString(t *testing.T, result core.Result) string {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(string)
-}
-
-func requireXcodeCloudFileInfo(t *testing.T, result core.Result) core.FsFileInfo {
- t.Helper()
- if !result.OK {
- t.Fatalf("unexpected error: %v", result.Error())
- }
- return result.Value.(core.FsFileInfo)
-}
-
-func requireXcodeCloudError(t *testing.T, result core.Result) string {
- t.Helper()
- if result.OK {
- t.Fatal("expected error")
- }
- return result.Error()
-}
-
-func TestXcodeCloud_HasXcodeCloudConfig_Good(t *testing.T) {
- if HasXcodeCloudConfig(nil) {
- t.Fatal("expected false")
- }
- if (HasXcodeCloudConfig(&BuildConfig{})) {
- t.Fatal("expected false")
- }
- if !(HasXcodeCloudConfig(&BuildConfig{Apple: AppleConfig{XcodeCloud: XcodeCloudConfig{Workflow: "CoreGUI Release"}}})) {
- t.Fatal("expected true")
- }
- if !(HasXcodeCloudConfig(&BuildConfig{Apple: AppleConfig{XcodeCloud: XcodeCloudConfig{Triggers: []XcodeCloudTrigger{{Branch: "main", Action: "testflight"}}}}})) {
- t.Fatal("expected true")
- }
-
-}
-
-func TestXcodeCloud_GenerateXcodeCloudScripts_Good(t *testing.T) {
- scripts := GenerateXcodeCloudScripts("/tmp/project", &BuildConfig{
- Project: Project{
- Name: "Core",
- Binary: "Core",
- },
- Apple: AppleConfig{
- BundleID: "ai.lthn.core",
- TeamID: "ABC123DEF4",
- Arch: "universal",
- Notarise: boolPtr(false),
- DMG: boolPtr(true),
- AppStore: boolPtr(true),
- },
- })
- if len(scripts) != 3 {
- t.Fatalf("want len %v, got %v", 3, len(scripts))
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], "go install github.com/wailsapp/wails/v3/cmd/wails3@latest") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], "go install github.com/wailsapp/wails/v3/cmd/wails3@latest")
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], "find_visible_files()") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], "find_visible_files()")
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], "-path './.*'") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], "-path './.*'")
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], "find_visible_files 3 -name package.json") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], "find_visible_files 3 -name package.json")
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], "package_manager_from_manifest()") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], "package_manager_from_manifest()")
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], "pkg.packageManager") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], "pkg.packageManager")
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], `declared_manager="$(package_manager_from_manifest "$dir")"`) {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], `declared_manager="$(package_manager_from_manifest "$dir")"`)
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], `(cd "$dir" && pnpm install)`) {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], `(cd "$dir" && pnpm install)`)
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], `(cd "$dir" && yarn install)`) {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], `(cd "$dir" && yarn install)`)
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], `(cd "$dir" && bun install)`) {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], `(cd "$dir" && bun install)`)
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], "deno_requested()") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], "deno_requested()")
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], "DENO_ENABLE") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], "DENO_ENABLE")
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostCloneScriptName], "DENO_BUILD") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostCloneScriptName], "DENO_BUILD")
- }
- if !stdlibAssertContains(scripts[XcodeCloudPreXcodebuildScriptName], `core build apple --arch 'universal' --config '.core/build.yaml' --notarise=false --dmg --appstore --bundle-id 'ai.lthn.core' --team-id 'ABC123DEF4'`) {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPreXcodebuildScriptName], `core build apple --arch 'universal' --config '.core/build.yaml' --notarise=false --dmg --appstore --bundle-id 'ai.lthn.core' --team-id 'ABC123DEF4'`)
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostXcodebuildScriptName], `BUNDLE_PATH='dist/apple/Core.app'`) {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostXcodebuildScriptName], `BUNDLE_PATH='dist/apple/Core.app'`)
- }
- if !stdlibAssertContains(scripts[XcodeCloudPostXcodebuildScriptName], "codesign --verify --deep --strict") {
- t.Fatalf("expected %v to contain %v", scripts[XcodeCloudPostXcodebuildScriptName], "codesign --verify --deep --strict")
- }
-
-}
-
-func TestXcodeCloud_GenerateXcodeCloudScripts_QuotesShellValues(t *testing.T) {
- scripts := GenerateXcodeCloudScripts("/tmp/project", &BuildConfig{
- Project: Project{
- Name: "Core",
- Binary: "Core$(touch /tmp/pwned)",
- },
- Apple: AppleConfig{
- BundleID: "ai.lthn.core$(touch /tmp/pwned)",
- TeamID: "ABC123DEF4$(touch /tmp/pwned)",
- Arch: "arm64$(touch /tmp/pwned)",
- },
- })
-
- pre := scripts[XcodeCloudPreXcodebuildScriptName]
- if !stdlibAssertContains(pre, `--arch 'arm64$(touch /tmp/pwned)'`) {
- t.Fatalf("expected %v to contain %v", pre, `--arch 'arm64$(touch /tmp/pwned)'`)
- }
- if !stdlibAssertContains(pre, `--bundle-id 'ai.lthn.core$(touch /tmp/pwned)'`) {
- t.Fatalf("expected %v to contain %v", pre, `--bundle-id 'ai.lthn.core$(touch /tmp/pwned)'`)
- }
- if !stdlibAssertContains(pre, `--team-id 'ABC123DEF4$(touch /tmp/pwned)'`) {
- t.Fatalf("expected %v to contain %v", pre, `--team-id 'ABC123DEF4$(touch /tmp/pwned)'`)
- }
- if stdlibAssertContains(pre, `--arch "arm64$(touch /tmp/pwned)"`) {
- t.Fatalf("expected %v not to contain %v", pre, `--arch "arm64$(touch /tmp/pwned)"`)
- }
- if stdlibAssertContains(pre, `--bundle-id "ai.lthn.core$(touch /tmp/pwned)"`) {
- t.Fatalf("expected %v not to contain %v", pre, `--bundle-id "ai.lthn.core$(touch /tmp/pwned)"`)
- }
- if stdlibAssertContains(pre, `--team-id "ABC123DEF4$(touch /tmp/pwned)"`) {
- t.Fatalf("expected %v not to contain %v", pre, `--team-id "ABC123DEF4$(touch /tmp/pwned)"`)
- }
-
- post := scripts[XcodeCloudPostXcodebuildScriptName]
- if !stdlibAssertContains(post, `BUNDLE_PATH='dist/apple/Core$(touch /tmp/pwned).app'`) {
- t.Fatalf("expected %v to contain %v", post, `BUNDLE_PATH='dist/apple/Core$(touch /tmp/pwned).app'`)
- }
- if !stdlibAssertContains(post, `EXECUTABLE_PATH='dist/apple/Core$(touch /tmp/pwned).app/Contents/MacOS/Core$(touch /tmp/pwned)'`) {
- t.Fatalf("expected %v to contain %v", post, `EXECUTABLE_PATH='dist/apple/Core$(touch /tmp/pwned).app/Contents/MacOS/Core$(touch /tmp/pwned)'`)
- }
- if stdlibAssertContains(post, `BUNDLE_PATH="dist/apple/Core$(touch /tmp/pwned).app"`) {
- t.Fatalf("expected %v not to contain %v", post, `BUNDLE_PATH="dist/apple/Core$(touch /tmp/pwned).app"`)
- }
- if stdlibAssertContains(post, `EXECUTABLE_PATH="dist/apple/Core$(touch /tmp/pwned).app/Contents/MacOS/Core$(touch /tmp/pwned)"`) {
- t.Fatalf("expected %v not to contain %v", post, `EXECUTABLE_PATH="dist/apple/Core$(touch /tmp/pwned).app/Contents/MacOS/Core$(touch /tmp/pwned)"`)
- }
-
-}
-
-func TestXcodeCloud_WriteXcodeCloudScripts_Good(t *testing.T) {
- projectDir := t.TempDir()
-
- paths := requireXcodeCloudPaths(t, WriteXcodeCloudScripts(storage.Local, projectDir, &BuildConfig{
- Project: Project{
- Name: "Core",
- Binary: "Core",
- },
- Apple: AppleConfig{
- XcodeCloud: XcodeCloudConfig{
- Workflow: "CoreGUI Release",
- },
- },
- }))
- if !stdlibAssertEqual([]string{ax.Join(projectDir, XcodeCloudScriptsDir, XcodeCloudPostCloneScriptName), ax.Join(projectDir, XcodeCloudScriptsDir, XcodeCloudPreXcodebuildScriptName), ax.Join(projectDir, XcodeCloudScriptsDir, XcodeCloudPostXcodebuildScriptName)}, paths) {
- t.Fatalf("want %v, got %v", []string{ax.Join(projectDir, XcodeCloudScriptsDir, XcodeCloudPostCloneScriptName), ax.Join(projectDir, XcodeCloudScriptsDir, XcodeCloudPreXcodebuildScriptName), ax.Join(projectDir, XcodeCloudScriptsDir, XcodeCloudPostXcodebuildScriptName)}, paths)
- }
-
- for _, path := range paths {
- content := requireXcodeCloudString(t, storage.Local.Read(path))
- if stdlibAssertEmpty(content) {
- t.Fatal("expected non-empty")
- }
-
- info := requireXcodeCloudFileInfo(t, storage.Local.Stat(path))
- if !stdlibAssertEqual(0o755, int(info.Mode().Perm())) {
- t.Fatalf("want %v, got %v", 0o755, int(info.Mode().Perm()))
- }
-
- }
-}
-
-func TestXcodeCloud_WriteXcodeCloudScripts_Bad(t *testing.T) {
- err := requireXcodeCloudError(t, WriteXcodeCloudScripts(nil, t.TempDir(), DefaultConfig()))
- if !stdlibAssertContains(err, "filesystem medium is required") {
- t.Fatalf("expected %v to contain %v", err, "filesystem medium is required")
- }
-
-}
-
-func boolPtr(value bool) *bool {
- return &value
-}
-
-// --- v0.9.0 generated compliance triplets ---
-func TestXcodeCloud_HasXcodeCloudConfig_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = HasXcodeCloudConfig(nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestXcodeCloud_HasXcodeCloudConfig_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = HasXcodeCloudConfig(&BuildConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestXcodeCloud_GenerateXcodeCloudScripts_Bad(t *core.T) {
- badCalls := 0
- core.AssertNotPanics(t, func() {
- _ = GenerateXcodeCloudScripts("", nil)
- badCalls++
- })
- core.AssertEqual(t, 1, badCalls)
-}
-
-func TestXcodeCloud_GenerateXcodeCloudScripts_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = GenerateXcodeCloudScripts(core.Path(t.TempDir(), "go-build-compliance"), &BuildConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
-
-func TestXcodeCloud_WriteXcodeCloudScripts_Ugly(t *core.T) {
- uglyCalls := 0
- core.AssertNotPanics(t, func() {
- _ = WriteXcodeCloudScripts(storage.NewMemoryMedium(), core.Path(t.TempDir(), "go-build-compliance"), &BuildConfig{})
- uglyCalls++
- })
- core.AssertEqual(t, 1, uglyCalls)
-}
diff --git a/pkg/release/output.go b/pkg/release/output.go
deleted file mode 100644
index 5c5c09a..0000000
--- a/pkg/release/output.go
+++ /dev/null
@@ -1,159 +0,0 @@
-package release
-
-import (
- stdio "io"
- "io/fs"
- "reflect"
-
- "dappco.re/go"
- "dappco.re/go/build/internal/ax"
- "dappco.re/go/build/pkg/build"
- coreio "dappco.re/go/build/pkg/storage"
-)
-
-func resolveReleaseOutputMedium(cfg *Config) coreio.Medium {
- if cfg == nil || cfg.output == nil {
- return coreio.Local
- }
- return cfg.output
-}
-
-func resolveReleaseOutputRoot(projectDir string, cfg *Config, output coreio.Medium) string {
- outputDir := ""
- if cfg != nil {
- outputDir = cfg.outputDir
- }
-
- if outputDir == "" && !mediumEquals(output, coreio.Local) {
- return ""
- }
-
- if outputDir == "" {
- outputDir = "dist"
- }
-
- if !ax.IsAbs(outputDir) && mediumEquals(output, coreio.Local) {
- return ax.Join(projectDir, outputDir)
- }
-
- return outputDir
-}
-
-func mirrorReleaseArtifacts(source, destination coreio.Medium, sourceRoot, destinationRoot string, artifacts []build.Artifact) core.Result {
- if source == nil {
- source = coreio.Local
- }
- if destination == nil {
- destination = coreio.Local
- }
-
- mirrored := make([]build.Artifact, 0, len(artifacts))
- for _, artifact := range artifacts {
- relativePathResult := ax.Rel(sourceRoot, artifact.Path)
- relativePath := ""
- if relativePathResult.OK {
- relativePath = relativePathResult.Value.(string)
- }
- if relativePath == "" || core.HasPrefix(relativePath, "..") {
- relativePath = ax.Base(artifact.Path)
- }
-
- destinationPath := joinReleasePath(destinationRoot, relativePath)
- copied := copyReleaseMediumPath(source, artifact.Path, destination, destinationPath)
- if !copied.OK {
- return core.Fail(core.E("release.mirrorReleaseArtifacts", "failed to mirror artifact "+artifact.Path, core.NewError(copied.Error())))
- }
-
- mirrored = append(mirrored, build.Artifact{
- Path: destinationPath,
- OS: artifact.OS,
- Arch: artifact.Arch,
- Checksum: artifact.Checksum,
- })
- }
-
- return core.Ok(mirrored)
-}
-
-func joinReleasePath(root, path string) string {
- if root == "" || root == "." {
- return ax.Clean(path)
- }
- if path == "" || path == "." {
- return ax.Clean(root)
- }
- return ax.Join(root, path)
-}
-
-func mediumEquals(left, right coreio.Medium) bool {
- if left == nil || right == nil {
- return left == nil && right == nil
- }
-
- leftType := reflect.TypeOf(left)
- rightType := reflect.TypeOf(right)
- if leftType != rightType || !leftType.Comparable() {
- return false
- }
-
- return reflect.ValueOf(left).Interface() == reflect.ValueOf(right).Interface()
-}
-
-func copyReleaseMediumPath(source coreio.Medium, sourcePath string, destination coreio.Medium, destinationPath string) core.Result {
- if source.IsDir(sourcePath) {
- return copyReleaseMediumDir(source, sourcePath, destination, destinationPath)
- }
-
- return copyReleaseMediumFile(source, sourcePath, destination, destinationPath)
-}
-
-func copyReleaseMediumDir(source coreio.Medium, sourcePath string, destination coreio.Medium, destinationPath string) core.Result {
- created := destination.EnsureDir(destinationPath)
- if !created.OK {
- return core.Fail(core.E("release.copyReleaseMediumDir", "failed to create destination directory", core.NewError(created.Error())))
- }
-
- entriesResult := source.List(sourcePath)
- if !entriesResult.OK {
- return core.Fail(core.E("release.copyReleaseMediumDir", "failed to list source directory", core.NewError(entriesResult.Error())))
- }
- entries := entriesResult.Value.([]fs.DirEntry)
-
- for _, entry := range entries {
- childSourcePath := ax.Join(sourcePath, entry.Name())
- childDestinationPath := ax.Join(destinationPath, entry.Name())
- copied := copyReleaseMediumPath(source, childSourcePath, destination, childDestinationPath)
- if !copied.OK {
- return copied
- }
- }
-
- return core.Ok(nil)
-}
-
-func copyReleaseMediumFile(source coreio.Medium, sourcePath string, destination coreio.Medium, destinationPath string) core.Result {
- fileResult := source.Open(sourcePath)
- if !fileResult.OK {
- return core.Fail(core.E("release.copyReleaseMediumFile", "failed to open source file", core.NewError(fileResult.Error())))
- }
- file := fileResult.Value.(core.FsFile)
- defer file.Close()
-
- content, readFailure := stdio.ReadAll(file)
- if readFailure != nil {
- return core.Fail(core.E("release.copyReleaseMediumFile", "failed to read source file", readFailure))
- }
-
- mode := fs.FileMode(0o644)
- infoResult := source.Stat(sourcePath)
- if infoResult.OK {
- mode = infoResult.Value.(fs.FileInfo).Mode()
- }
-
- written := destination.WriteMode(destinationPath, string(content), mode)
- if !written.OK {
- return core.Fail(core.E("release.copyReleaseMediumFile", "failed to write destination file", core.NewError(written.Error())))
- }
-
- return core.Ok(nil)
-}
diff --git a/pkg/release/testdata/.core/release.yaml b/pkg/release/testdata/.core/release.yaml
deleted file mode 100644
index b9c9fd7..0000000
--- a/pkg/release/testdata/.core/release.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-version: 1
-
-project:
- name: myapp
- repository: owner/repo
-
-build:
- targets:
- - os: linux
- arch: amd64
- - os: linux
- arch: arm64
- - os: darwin
- arch: amd64
- - os: darwin
- arch: arm64
- - os: windows
- arch: amd64
-
-publishers:
- - type: github
- prerelease: false
- draft: false
-
-changelog:
- include:
- - feat
- - fix
- - perf
- exclude:
- - chore
- - docs
- - style
- - test
- - ci
diff --git a/pkg/sdk/generators/stdlib_assert_test.go b/pkg/sdk/generators/stdlib_assert_test.go
deleted file mode 100644
index a608d4e..0000000
--- a/pkg/sdk/generators/stdlib_assert_test.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package generators
-
-import "dappco.re/go/build/internal/testassert"
-
-var (
- stdlibAssertEqual = testassert.Equal
- stdlibAssertNil = testassert.Nil
- stdlibAssertEmpty = testassert.Empty
- stdlibAssertZero = testassert.Zero
- stdlibAssertContains = testassert.Contains
- stdlibAssertElementsMatch = testassert.ElementsMatch
-)
diff --git a/sonar-project.properties b/sonar-project.properties
index 652b24c..4f8f7b3 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -5,4 +5,4 @@ sonar.exclusions=**/vendor/**,**/third_party/**,**/.tmp/**,**/gomodcache/**,**/n
sonar.tests=.
sonar.test.inclusions=**/*_test.go,**/*.test.ts,**/*.test.js,**/*.spec.ts,**/*.spec.js
sonar.test.exclusions=**/vendor/**,**/third_party/**,**/.tmp/**,**/gomodcache/**,**/node_modules/**,**/dist/**,**/build/**
-sonar.go.coverage.reportPaths=coverage.out
+sonar.go.coverage.reportPaths=go/coverage.out
diff --git a/tests/cli/build/Taskfile.yaml b/tests/cli/build/Taskfile.yaml
deleted file mode 100644
index 4ed396d..0000000
--- a/tests/cli/build/Taskfile.yaml
+++ /dev/null
@@ -1,174 +0,0 @@
-version: "3"
-
-tasks:
- test:
- desc: Validate AX-10 build CLI binary behaviour.
- cmds:
- - |
- bash <<'EOF'
- set -euo pipefail
-
- run_capture_all() {
- local expected_status="$1"
- local output_file="$2"
- shift 2
-
- set +e
- "$@" >"$output_file" 2>&1
- local status=$?
- set -e
-
- if [[ "$status" -ne "$expected_status" ]]; then
- printf 'expected exit %s, got %s\n' "$expected_status" "$status" >&2
- if [[ -s "$output_file" ]]; then
- printf 'output:\n' >&2
- cat "$output_file" >&2
- fi
- return 1
- fi
- }
-
- assert_contains() {
- local needle="$1"
- local input_file="$2"
-
- if ! grep -Fq "$needle" "$input_file"; then
- printf 'expected output to contain %q\n' "$needle" >&2
- if [[ -s "$input_file" ]]; then
- printf 'output:\n' >&2
- cat "$input_file" >&2
- fi
- return 1
- fi
- }
-
- repo_root="$(cd ../../.. && pwd)"
- core_root="$(cd "$repo_root/../go" && pwd)"
- work="$(mktemp -d)"
- module_dir="$work/module"
- cache_dir="$work/cache"
- cleanup() {
- chmod -R u+w "$work" 2>/dev/null || true
- rm -rf "$work"
- }
- trap cleanup EXIT
- mkdir -p "$module_dir" "$cache_dir"
-
- export GOWORK=off
- export GOCACHE="$cache_dir/gocache"
- export GOMODCACHE="$cache_dir/gomodcache"
- export GOPATH="$cache_dir/gopath"
- export GONOSUMDB="${GONOSUMDB:-dappco.re/*,forge.lthn.ai/*}"
- export DIR_HOME="$work/home"
- export HOME="$work/home"
- export NO_COLOR=1
- mkdir -p "$DIR_HOME"
-
- cat >"$module_dir/go.mod" < $repo_root
- replace dappco.re/go/core => $core_root
- EOM
-
- cat >"$module_dir/main.go" <<'EOGO'
- package main
-
- import (
- "fmt"
- "os"
-
- buildcmd "dappco.re/go/build/cmd/build"
- "dappco.re/go/core"
- )
-
- const version = "0.0.0-test"
-
- func main() {
- c := core.New(core.WithOption("name", "build"))
- c.App().Version = version
- buildcmd.AddBuildCommands(c)
- c.Command("version", core.Command{
- Description: "Show version",
- Action: func(_ core.Options) core.Result {
- fmt.Printf("build %s\n", version)
- return core.Result{OK: true}
- },
- })
- c.Cli().SetBanner(func(_ *core.Cli) string {
- return "build " + version
- })
-
- args := os.Args[1:]
- if len(args) > 0 {
- switch args[0] {
- case "--help", "-h":
- c.Cli().PrintHelp()
- return
- case "--version", "-v":
- args[0] = "version"
- }
- }
-
- result := c.Cli().Run(args...)
- if !result.OK {
- if err, ok := result.Value.(error); ok {
- fmt.Fprintln(os.Stderr, err.Error())
- os.Exit(1)
- }
- }
- }
- EOGO
-
- go mod tidy -C "$module_dir"
- go build -C "$module_dir" -trimpath -ldflags="-s -w" -o "$work/build" .
-
- output="$work/help.out"
- run_capture_all 0 "$output" "$work/build" --help
- assert_contains "build commands:" "$output"
- assert_contains "build/sdk" "$output"
- assert_contains "build/image" "$output"
- assert_contains "build/workflow" "$output"
- assert_contains "release" "$output"
-
- output="$work/version.out"
- run_capture_all 0 "$output" "$work/build" --version
- assert_contains "build 0.0.0-test" "$output"
-
- output="$work/image.out"
- run_capture_all 0 "$output" "$work/build" build/image --list
- assert_contains "Images" "$output"
- assert_contains "available immutable LinuxKit bases" "$output"
- assert_contains "core-dev" "$output"
-
- project="$work/project"
- mkdir -p "$project"
- cat >"$project/openapi.yaml" <<'EOSPEC'
- openapi: "3.0.0"
- info:
- title: Test API
- version: "1.0.0"
- paths:
- /health:
- get:
- operationId: getHealth
- responses:
- "200":
- description: OK
- EOSPEC
-
- output="$work/sdk.out"
- (
- cd "$project"
- run_capture_all 0 "$output" "$work/build" build/sdk --dry-run --spec=openapi.yaml --lang=go
- )
- assert_contains "SDK" "$output"
- assert_contains "Dry run" "$output"
- assert_contains "Spec:" "$output"
- assert_contains "Language: go" "$output"
- assert_contains "Would generate SDK" "$output"
- EOF