From 73debaae97767e6921b79e5c2bcb45544caf94fa Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 30 Apr 2026 12:22:20 +0100 Subject: [PATCH 1/3] chore(go-build): restructure Go module under go/ + go.work + external/ submodule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lift Go module into go/ subtree for cross-language repo symmetry (matches core/api v0.12.0 shape). Module path stays dappco.re/go/build — consumers see no change. Adds go.work + external/go submodule for dev workspace mode; CI uses GOWORK=off (already set in .woodpecker.yml). Moved into go/: - go.mod, go.sum - cmd/, pkg/, internal/, tests/, locales/ (locales/embed.go imports as a Go package so must live inside the module) Stays at repo root: - README.md, CLAUDE.md, AGENTS.md, LICENSE, sonar-project.properties, .woodpecker.yml, docs/, ui/ Symlinks: go/{README,CLAUDE,AGENTS}.md + go/docs → ../ Pre-existing failure (NOT caused by restructure): - pkg/service TestDaemon_Run_{Good,Bad,Ugly} fails with gin handler re-registration panic on shared router — test isolation issue. Tracked as separate concern. Verification: - go mod tidy clean (GOWORK=off) - go vet ./... clean - go test pass for pkg/storage; pre-existing pkg/service flake noted above - audit COMPLIANT --- .gitmodules | 4 + .woodpecker.yml | 4 +- CLAUDE.md | 42 + cmd/build/ci_output.go | 20 - cmd/build/ci_output_test.go | 10 - cmd/build/cmd_apple.go | 266 -- cmd/build/cmd_apple_example_test.go | 10 - cmd/build/cmd_apple_test.go | 369 --- cmd/build/cmd_build.go | 165 -- cmd/build/cmd_build_example_test.go | 10 - cmd/build/cmd_build_test.go | 24 - cmd/build/cmd_commands.go | 5 - cmd/build/cmd_image.go | 584 ---- cmd/build/cmd_image_example_test.go | 10 - cmd/build/cmd_image_test.go | 385 --- cmd/build/cmd_installers.go | 204 -- cmd/build/cmd_installers_example_test.go | 10 - cmd/build/cmd_installers_test.go | 235 -- cmd/build/cmd_project.go | 805 ------ cmd/build/cmd_project_example_test.go | 11 - cmd/build/cmd_project_test.go | 964 ------- cmd/build/cmd_pwa.go | 814 ------ cmd/build/cmd_pwa_test.go | 186 -- cmd/build/cmd_release.go | 208 -- cmd/build/cmd_release_example_test.go | 10 - cmd/build/cmd_release_test.go | 278 -- cmd/build/cmd_sdk.go | 116 - cmd/build/cmd_sdk_test.go | 61 - cmd/build/cmd_service.go | 208 -- cmd/build/cmd_service_example_test.go | 10 - cmd/build/cmd_service_test.go | 222 -- cmd/build/cmd_workflow.go | 176 -- cmd/build/cmd_workflow_example_test.go | 10 - cmd/build/cmd_workflow_test.go | 344 --- cmd/build/tmpl/gui/go.mod.tmpl | 7 - cmd/build/tmpl/gui/html/.gitkeep | 0 cmd/build/tmpl/gui/html/.placeholder | 1 - cmd/build/tmpl/gui/main.go.tmpl | 25 - external/go | 1 + go.work | 9 + go/AGENTS.md | 1 + go/CLAUDE.md | 1 + go/README.md | 1 + {cmd => go/cmd}/ci/ci.go | 0 {cmd => go/cmd}/ci/ci_test.go | 0 {cmd => go/cmd}/ci/cmd.go | 0 {cmd => go/cmd}/ci/cmd_example_test.go | 0 {cmd => go/cmd}/ci/cmd_test.go | 0 {cmd => go/cmd}/ci/stdlib_assert_test.go | 0 {cmd => go/cmd}/sdk/cmd.go | 0 {cmd => go/cmd}/sdk/cmd_example_test.go | 0 {cmd => go/cmd}/sdk/cmd_test.go | 0 {cmd => go/cmd}/sdk/stdlib_assert_test.go | 0 {cmd => go/cmd}/service/cmd.go | 0 {cmd => go/cmd}/service/cmd_example_test.go | 0 {cmd => go/cmd}/service/cmd_test.go | 0 {cmd => go/cmd}/service/stdlib_assert_test.go | 0 go/docs | 1 + go.mod => go/go.mod | 0 go.sum => go/go.sum | 0 {internal => go/internal}/ax/ax.go | 0 .../internal}/ax/ax_example_test.go | 0 {internal => go/internal}/ax/ax_test.go | 0 .../internal}/buildtest/workflow.go | 0 .../buildtest/workflow_example_test.go | 0 .../internal}/buildtest/workflow_test.go | 0 {internal => go/internal}/cli/cli.go | 0 .../internal}/cli/cli_example_test.go | 0 {internal => go/internal}/cli/cli_test.go | 0 {internal => go/internal}/cmdutil/cmdutil.go | 0 .../internal}/cmdutil/cmdutil_example_test.go | 0 .../internal}/cmdutil/cmdutil_test.go | 0 .../internal}/projectdetect/projectdetect.go | 0 .../projectdetect_example_test.go | 0 .../projectdetect/projectdetect_test.go | 0 .../projectdetect/stdlib_assert_test.go | 0 {internal => go/internal}/sdkcfg/sdkcfg.go | 0 .../internal}/sdkcfg/sdkcfg_example_test.go | 0 .../internal}/sdkcfg/sdkcfg_test.go | 0 .../internal}/sdkcfg/stdlib_assert_test.go | 0 .../internal}/servicecmd/request.go | 0 .../servicecmd/request_example_test.go | 0 .../internal}/servicecmd/request_test.go | 0 .../internal}/testassert/testassert.go | 0 .../testassert/testassert_example_test.go | 0 .../internal}/testassert/testassert_test.go | 0 {locales => go/locales}/embed.go | 0 {locales => go/locales}/en.json | 0 {pkg => go/pkg}/api/embed.go | 0 {pkg => go/pkg}/api/http.go | 0 {pkg => go/pkg}/api/http_example_test.go | 0 {pkg => go/pkg}/api/http_test.go | 0 {pkg => go/pkg}/api/provider.go | 0 {pkg => go/pkg}/api/provider/provider.go | 0 .../api/provider/provider_example_test.go | 0 {pkg => go/pkg}/api/provider/provider_test.go | 0 {pkg => go/pkg}/api/provider_example_test.go | 0 {pkg => go/pkg}/api/provider_test.go | 0 {pkg => go/pkg}/api/stdlib_assert_test.go | 0 {pkg => go/pkg}/events/events.go | 0 {pkg => go/pkg}/events/events_example_test.go | 0 {pkg => go/pkg}/events/events_test.go | 0 {pkg => go/pkg}/release/changelog.go | 0 .../pkg}/release/changelog_example_test.go | 0 {pkg => go/pkg}/release/changelog_test.go | 0 {pkg => go/pkg}/release/config.go | 0 .../pkg}/release/config_example_test.go | 0 {pkg => go/pkg}/release/config_test.go | 0 {pkg => go/pkg}/release/output.go | 0 {pkg => go/pkg}/release/publishers/assets.go | 0 .../release/publishers/assets_example_test.go | 0 .../pkg}/release/publishers/assets_test.go | 0 {pkg => go/pkg}/release/publishers/aur.go | 0 .../release/publishers/aur_example_test.go | 0 .../pkg}/release/publishers/aur_test.go | 0 .../pkg}/release/publishers/chocolatey.go | 0 .../publishers/chocolatey_example_test.go | 0 .../release/publishers/chocolatey_test.go | 0 {pkg => go/pkg}/release/publishers/docker.go | 0 .../release/publishers/docker_example_test.go | 0 .../pkg}/release/publishers/docker_test.go | 0 {pkg => go/pkg}/release/publishers/github.go | 0 .../release/publishers/github_example_test.go | 0 .../pkg}/release/publishers/github_test.go | 0 .../pkg}/release/publishers/homebrew.go | 0 .../publishers/homebrew_example_test.go | 0 .../pkg}/release/publishers/homebrew_test.go | 0 .../release/publishers/integration_test.go | 0 .../pkg}/release/publishers/linuxkit.go | 0 .../pkg}/release/publishers/linuxkit_aws.go | 0 .../publishers/linuxkit_example_test.go | 0 .../pkg}/release/publishers/linuxkit_gcp.go | 0 .../pkg}/release/publishers/linuxkit_iso.go | 0 .../pkg}/release/publishers/linuxkit_qcow2.go | 0 .../pkg}/release/publishers/linuxkit_raw.go | 0 .../pkg}/release/publishers/linuxkit_test.go | 0 {pkg => go/pkg}/release/publishers/npm.go | 0 .../release/publishers/npm_example_test.go | 0 .../pkg}/release/publishers/npm_test.go | 0 {pkg => go/pkg}/release/publishers/output.go | 0 .../pkg}/release/publishers/publisher.go | 0 .../publishers/publisher_example_test.go | 0 .../pkg}/release/publishers/publisher_test.go | 0 {pkg => go/pkg}/release/publishers/scoop.go | 0 .../release/publishers/scoop_example_test.go | 0 .../pkg}/release/publishers/scoop_test.go | 0 .../release/publishers/stdlib_assert_test.go | 0 .../pkg}/release/publishers/template_funcs.go | 0 .../publishers/templates/aur/.SRCINFO.tmpl | 0 .../publishers/templates/aur/PKGBUILD.tmpl | 0 .../templates/chocolatey/package.nuspec.tmpl | 0 .../tools/chocolateyinstall.ps1.tmpl | 0 .../templates/homebrew/formula.rb.tmpl | 0 .../publishers/templates/npm/install.js.tmpl | 0 .../templates/npm/package.json.tmpl | 0 .../publishers/templates/npm/run.js.tmpl | 0 .../templates/scoop/manifest.json.tmpl | 0 .../release/publishers/test_helpers_test.go | 0 .../publishers/version_validation_test.go | 0 {pkg => go/pkg}/release/release.go | 0 .../pkg}/release/release_example_test.go | 0 {pkg => go/pkg}/release/release_test.go | 0 {pkg => go/pkg}/release/sdk.go | 0 {pkg => go/pkg}/release/sdk_example_test.go | 0 {pkg => go/pkg}/release/sdk_test.go | 0 {pkg => go/pkg}/release/stdlib_assert_test.go | 0 {pkg => go/pkg}/release/test_helpers_test.go | 0 {pkg => go/pkg}/release/version.go | 0 .../pkg}/release/version_example_test.go | 0 {pkg => go/pkg}/release/version_test.go | 0 {pkg => go/pkg}/sdk/breaking_test.go | 0 {pkg => go/pkg}/sdk/detect.go | 0 {pkg => go/pkg}/sdk/detect_example_test.go | 0 {pkg => go/pkg}/sdk/detect_test.go | 0 {pkg => go/pkg}/sdk/diff.go | 0 {pkg => go/pkg}/sdk/diff_example_test.go | 0 {pkg => go/pkg}/sdk/diff_test.go | 0 {pkg => go/pkg}/sdk/generation_test.go | 0 .../pkg}/sdk/generators/docker_runtime.go | 0 .../sdk/generators/docker_runtime_test.go | 0 {pkg => go/pkg}/sdk/generators/generator.go | 0 .../sdk/generators/generator_example_test.go | 0 .../pkg}/sdk/generators/generator_test.go | 0 {pkg => go/pkg}/sdk/generators/go.go | 0 .../pkg}/sdk/generators/go_example_test.go | 0 {pkg => go/pkg}/sdk/generators/go_test.go | 0 {pkg => go/pkg}/sdk/generators/php.go | 0 .../pkg}/sdk/generators/php_example_test.go | 0 {pkg => go/pkg}/sdk/generators/php_test.go | 0 {pkg => go/pkg}/sdk/generators/python.go | 0 .../sdk/generators/python_example_test.go | 0 {pkg => go/pkg}/sdk/generators/python_test.go | 0 .../pkg}/sdk/generators/stdlib_assert_test.go | 0 {pkg => go/pkg}/sdk/generators/typescript.go | 0 .../sdk/generators/typescript_example_test.go | 0 .../pkg}/sdk/generators/typescript_test.go | 0 {pkg => go/pkg}/sdk/sdk.go | 0 {pkg => go/pkg}/sdk/sdk_example_test.go | 0 {pkg => go/pkg}/sdk/sdk_test.go | 0 {pkg => go/pkg}/sdk/stdlib_assert_test.go | 0 {pkg => go/pkg}/sdk/validate.go | 0 {pkg => go/pkg}/sdk/validate_example_test.go | 0 {pkg => go/pkg}/sdk/validate_test.go | 0 {pkg => go/pkg}/service/agentic.go | 0 .../pkg}/service/agentic_example_test.go | 0 {pkg => go/pkg}/service/agentic_test.go | 0 {pkg => go/pkg}/service/config.go | 0 .../pkg}/service/config_example_test.go | 0 {pkg => go/pkg}/service/config_test.go | 0 {pkg => go/pkg}/service/daemon.go | 0 .../pkg}/service/daemon_example_test.go | 0 {pkg => go/pkg}/service/daemon_run_test.go | 0 {pkg => go/pkg}/service/daemon_test.go | 0 {pkg => go/pkg}/service/export.go | 0 .../pkg}/service/export_example_test.go | 0 {pkg => go/pkg}/service/export_test.go | 0 {pkg => go/pkg}/service/manager.go | 0 .../pkg}/service/manager_example_test.go | 0 {pkg => go/pkg}/service/manager_test.go | 0 {pkg => go/pkg}/service/mcp.go | 0 {pkg => go/pkg}/service/mcp_test.go | 0 {pkg => go/pkg}/service/process_daemon.go | 0 {pkg => go/pkg}/service/stdlib_assert_test.go | 0 {pkg => go/pkg}/service/test_helpers_test.go | 0 {pkg => go/pkg}/storage/storage.go | 0 .../pkg}/storage/storage_example_test.go | 0 {pkg => go/pkg}/storage/storage_test.go | 0 pkg/api/ui/dist/core-build.js | 2496 ----------------- pkg/build/apple.go | 2461 ---------------- pkg/build/apple/apple.go | 589 ---- pkg/build/apple/apple_example_test.go | 129 - pkg/build/apple/apple_test.go | 1142 -------- pkg/build/apple_example_test.go | 101 - pkg/build/apple_test.go | 1591 ----------- pkg/build/archive.go | 397 --- pkg/build/archive_example_test.go | 52 - pkg/build/archive_test.go | 1028 ------- pkg/build/build.go | 136 - pkg/build/build_example_test.go | 10 - pkg/build/build_test.go | 24 - pkg/build/builders/apple.go | 627 ----- pkg/build/builders/apple_dmg.go | 109 - pkg/build/builders/apple_dmg_example_test.go | 10 - pkg/build/builders/apple_dmg_test.go | 34 - pkg/build/builders/apple_example_test.go | 101 - pkg/build/builders/apple_notarise.go | 72 - .../builders/apple_notarise_example_test.go | 10 - pkg/build/builders/apple_notarise_test.go | 32 - pkg/build/builders/apple_plist.go | 286 -- .../builders/apple_plist_example_test.go | 52 - pkg/build/builders/apple_plist_test.go | 151 - pkg/build/builders/apple_test.go | 626 ----- pkg/build/builders/cpp.go | 539 ---- pkg/build/builders/cpp_example_test.go | 31 - pkg/build/builders/cpp_test.go | 677 ----- pkg/build/builders/deno.go | 120 - pkg/build/builders/deno_test.go | 263 -- pkg/build/builders/docker.go | 235 -- pkg/build/builders/docker_example_test.go | 31 - pkg/build/builders/docker_test.go | 549 ---- pkg/build/builders/docs.go | 148 - pkg/build/builders/docs_example_test.go | 31 - pkg/build/builders/docs_test.go | 364 --- pkg/build/builders/env.go | 273 -- pkg/build/builders/go.go | 267 -- pkg/build/builders/go_example_test.go | 31 - pkg/build/builders/go_test.go | 1376 --------- pkg/build/builders/linuxkit.go | 324 --- pkg/build/builders/linuxkit_example_test.go | 31 - pkg/build/builders/linuxkit_image.go | 503 ---- .../builders/linuxkit_image_example_test.go | 38 - pkg/build/builders/linuxkit_image_test.go | 372 --- pkg/build/builders/linuxkit_test.go | 663 ----- pkg/build/builders/node.go | 338 --- pkg/build/builders/node_example_test.go | 31 - pkg/build/builders/node_test.go | 817 ------ pkg/build/builders/package_manager.go | 50 - pkg/build/builders/php.go | 205 -- pkg/build/builders/php_example_test.go | 31 - pkg/build/builders/php_test.go | 408 --- pkg/build/builders/python.go | 109 - pkg/build/builders/python_example_test.go | 31 - pkg/build/builders/python_test.go | 327 --- pkg/build/builders/resolver.go | 44 - pkg/build/builders/resolver_example_test.go | 10 - pkg/build/builders/resolver_init_test.go | 27 - pkg/build/builders/resolver_test.go | 73 - pkg/build/builders/rust.go | 192 -- pkg/build/builders/rust_example_test.go | 31 - pkg/build/builders/rust_test.go | 325 --- pkg/build/builders/taskfile.go | 313 --- pkg/build/builders/taskfile_example_test.go | 31 - pkg/build/builders/taskfile_test.go | 845 ------ pkg/build/builders/wails.go | 1075 ------- pkg/build/builders/wails_example_test.go | 38 - pkg/build/builders/wails_test.go | 2207 --------------- pkg/build/builders/zip_deterministic.go | 5 - pkg/build/builtin_resolver.go | 228 -- pkg/build/builtin_resolver_example_test.go | 26 - pkg/build/builtin_resolver_test.go | 96 - pkg/build/cache.go | 401 --- pkg/build/cache_example_test.go | 66 - pkg/build/cache_test.go | 581 ---- pkg/build/checksum.go | 121 - pkg/build/checksum_example_test.go | 24 - pkg/build/checksum_test.go | 408 --- pkg/build/ci.go | 375 --- pkg/build/ci_example_test.go | 45 - pkg/build/ci_test.go | 720 ----- pkg/build/config.go | 1064 ------- pkg/build/config_example_test.go | 122 - pkg/build/config_test.go | 1885 ------------- pkg/build/discovery.go | 944 ------- pkg/build/discovery_example_test.go | 143 - pkg/build/discovery_test.go | 2362 ---------------- pkg/build/env.go | 60 - pkg/build/env_example_test.go | 24 - pkg/build/env_test.go | 90 - pkg/build/images/core-dev.yml | 42 - pkg/build/images/core-minimal.yml | 40 - pkg/build/images/core-ml.yml | 42 - pkg/build/installers.go | 82 - pkg/build/installers/installer.go | 283 -- .../installers/installer_example_test.go | 31 - pkg/build/installers/installer_test.go | 396 --- pkg/build/installers/templates/agent.sh.tmpl | 85 - pkg/build/installers/templates/ci.sh.tmpl | 73 - pkg/build/installers/templates/dev.sh.tmpl | 69 - pkg/build/installers/templates/go.sh.tmpl | 78 - pkg/build/installers/templates/php.sh.tmpl | 78 - pkg/build/installers/templates/setup.sh.tmpl | 143 - pkg/build/installers_example_test.go | 45 - pkg/build/installers_test.go | 119 - pkg/build/linuxkit_image.go | 173 -- pkg/build/linuxkit_image_example_test.go | 59 - pkg/build/linuxkit_image_test.go | 303 -- pkg/build/linuxkit_templates.go | 82 - pkg/build/linuxkit_templates_example_test.go | 24 - pkg/build/linuxkit_templates_test.go | 60 - pkg/build/options.go | 224 -- pkg/build/options_example_test.go | 31 - pkg/build/options_test.go | 652 ----- pkg/build/pipeline.go | 440 --- pkg/build/pipeline_example_test.go | 24 - pkg/build/pipeline_test.go | 643 ----- pkg/build/run.go | 422 --- pkg/build/run_example_test.go | 164 -- pkg/build/run_test.go | 957 ------- pkg/build/runtime_config.go | 130 - pkg/build/runtime_config_example_test.go | 10 - pkg/build/runtime_config_test.go | 274 -- pkg/build/setup.go | 302 -- pkg/build/setup_example_test.go | 17 - pkg/build/setup_test.go | 266 -- pkg/build/signing/codesign.go | 182 -- pkg/build/signing/codesign_example_test.go | 45 - pkg/build/signing/codesign_test.go | 376 --- pkg/build/signing/gpg.go | 88 - pkg/build/signing/gpg_example_test.go | 31 - pkg/build/signing/gpg_test.go | 209 -- pkg/build/signing/sign.go | 125 - pkg/build/signing/sign_example_test.go | 24 - pkg/build/signing/sign_test.go | 71 - pkg/build/signing/signer.go | 160 -- pkg/build/signing/signer_example_test.go | 24 - pkg/build/signing/signer_test.go | 92 - pkg/build/signing/signing_test.go | 486 ---- pkg/build/signing/signtool.go | 109 - pkg/build/signing/signtool_example_test.go | 31 - pkg/build/signing/signtool_test.go | 226 -- pkg/build/templates/release.yml | 990 ------- .../testdata/config-project/.core/build.yaml | 25 - pkg/build/testdata/cpp-project/CMakeLists.txt | 2 - pkg/build/testdata/docs-project/mkdocs.yml | 1 - pkg/build/testdata/empty-project/.gitkeep | 0 pkg/build/testdata/go-project/go.mod | 3 - .../monorepo-project/apps/web/package.json | 1 - pkg/build/testdata/multi-project/go.mod | 3 - pkg/build/testdata/multi-project/package.json | 4 - pkg/build/testdata/node-project/package.json | 4 - pkg/build/testdata/php-project/composer.json | 4 - .../testdata/python-project/pyproject.toml | 1 - pkg/build/testdata/rust-project/Cargo.toml | 1 - pkg/build/testdata/wails-project/go.mod | 3 - pkg/build/testdata/wails-project/wails.json | 4 - pkg/build/version.go | 36 - pkg/build/version_example_test.go | 17 - pkg/build/version_flags.go | 22 - pkg/build/version_flags_example_test.go | 10 - pkg/build/version_flags_test.go | 105 - pkg/build/version_templates.go | 57 - pkg/build/version_templates_example_test.go | 24 - pkg/build/version_templates_test.go | 122 - pkg/build/version_test.go | 100 - pkg/build/workflow.go | 529 ---- pkg/build/workflow_example_test.go | 80 - pkg/build/workflow_test.go | 835 ------ pkg/build/xcode_cloud.go | 357 --- pkg/build/xcode_cloud_example_test.go | 24 - pkg/build/xcode_cloud_test.go | 265 -- pkg/release/testdata/.core/release.yaml | 35 - sonar-project.properties | 2 +- tests/cli/build/Taskfile.yaml | 174 -- 403 files changed, 63 insertions(+), 56226 deletions(-) create mode 100644 .gitmodules delete mode 100644 cmd/build/ci_output.go delete mode 100644 cmd/build/ci_output_test.go delete mode 100644 cmd/build/cmd_apple.go delete mode 100644 cmd/build/cmd_apple_example_test.go delete mode 100644 cmd/build/cmd_apple_test.go delete mode 100644 cmd/build/cmd_build.go delete mode 100644 cmd/build/cmd_build_example_test.go delete mode 100644 cmd/build/cmd_build_test.go delete mode 100644 cmd/build/cmd_commands.go delete mode 100644 cmd/build/cmd_image.go delete mode 100644 cmd/build/cmd_image_example_test.go delete mode 100644 cmd/build/cmd_image_test.go delete mode 100644 cmd/build/cmd_installers.go delete mode 100644 cmd/build/cmd_installers_example_test.go delete mode 100644 cmd/build/cmd_installers_test.go delete mode 100644 cmd/build/cmd_project.go delete mode 100644 cmd/build/cmd_project_example_test.go delete mode 100644 cmd/build/cmd_project_test.go delete mode 100644 cmd/build/cmd_pwa.go delete mode 100644 cmd/build/cmd_pwa_test.go delete mode 100644 cmd/build/cmd_release.go delete mode 100644 cmd/build/cmd_release_example_test.go delete mode 100644 cmd/build/cmd_release_test.go delete mode 100644 cmd/build/cmd_sdk.go delete mode 100644 cmd/build/cmd_sdk_test.go delete mode 100644 cmd/build/cmd_service.go delete mode 100644 cmd/build/cmd_service_example_test.go delete mode 100644 cmd/build/cmd_service_test.go delete mode 100644 cmd/build/cmd_workflow.go delete mode 100644 cmd/build/cmd_workflow_example_test.go delete mode 100644 cmd/build/cmd_workflow_test.go delete mode 100644 cmd/build/tmpl/gui/go.mod.tmpl delete mode 100644 cmd/build/tmpl/gui/html/.gitkeep delete mode 100644 cmd/build/tmpl/gui/html/.placeholder delete mode 100644 cmd/build/tmpl/gui/main.go.tmpl create mode 160000 external/go create mode 100644 go.work create mode 120000 go/AGENTS.md create mode 120000 go/CLAUDE.md create mode 120000 go/README.md rename {cmd => go/cmd}/ci/ci.go (100%) rename {cmd => go/cmd}/ci/ci_test.go (100%) rename {cmd => go/cmd}/ci/cmd.go (100%) rename {cmd => go/cmd}/ci/cmd_example_test.go (100%) rename {cmd => go/cmd}/ci/cmd_test.go (100%) rename {cmd => go/cmd}/ci/stdlib_assert_test.go (100%) rename {cmd => go/cmd}/sdk/cmd.go (100%) rename {cmd => go/cmd}/sdk/cmd_example_test.go (100%) rename {cmd => go/cmd}/sdk/cmd_test.go (100%) rename {cmd => go/cmd}/sdk/stdlib_assert_test.go (100%) rename {cmd => go/cmd}/service/cmd.go (100%) rename {cmd => go/cmd}/service/cmd_example_test.go (100%) rename {cmd => go/cmd}/service/cmd_test.go (100%) rename {cmd => go/cmd}/service/stdlib_assert_test.go (100%) create mode 120000 go/docs rename go.mod => go/go.mod (100%) rename go.sum => go/go.sum (100%) rename {internal => go/internal}/ax/ax.go (100%) rename {internal => go/internal}/ax/ax_example_test.go (100%) rename {internal => go/internal}/ax/ax_test.go (100%) rename {internal => go/internal}/buildtest/workflow.go (100%) rename {internal => go/internal}/buildtest/workflow_example_test.go (100%) rename {internal => go/internal}/buildtest/workflow_test.go (100%) rename {internal => go/internal}/cli/cli.go (100%) rename {internal => go/internal}/cli/cli_example_test.go (100%) rename {internal => go/internal}/cli/cli_test.go (100%) rename {internal => go/internal}/cmdutil/cmdutil.go (100%) rename {internal => go/internal}/cmdutil/cmdutil_example_test.go (100%) rename {internal => go/internal}/cmdutil/cmdutil_test.go (100%) rename {internal => go/internal}/projectdetect/projectdetect.go (100%) rename {internal => go/internal}/projectdetect/projectdetect_example_test.go (100%) rename {internal => go/internal}/projectdetect/projectdetect_test.go (100%) rename {internal => go/internal}/projectdetect/stdlib_assert_test.go (100%) rename {internal => go/internal}/sdkcfg/sdkcfg.go (100%) rename {internal => go/internal}/sdkcfg/sdkcfg_example_test.go (100%) rename {internal => go/internal}/sdkcfg/sdkcfg_test.go (100%) rename {internal => go/internal}/sdkcfg/stdlib_assert_test.go (100%) rename {internal => go/internal}/servicecmd/request.go (100%) rename {internal => go/internal}/servicecmd/request_example_test.go (100%) rename {internal => go/internal}/servicecmd/request_test.go (100%) rename {internal => go/internal}/testassert/testassert.go (100%) rename {internal => go/internal}/testassert/testassert_example_test.go (100%) rename {internal => go/internal}/testassert/testassert_test.go (100%) rename {locales => go/locales}/embed.go (100%) rename {locales => go/locales}/en.json (100%) rename {pkg => go/pkg}/api/embed.go (100%) rename {pkg => go/pkg}/api/http.go (100%) rename {pkg => go/pkg}/api/http_example_test.go (100%) rename {pkg => go/pkg}/api/http_test.go (100%) rename {pkg => go/pkg}/api/provider.go (100%) rename {pkg => go/pkg}/api/provider/provider.go (100%) rename {pkg => go/pkg}/api/provider/provider_example_test.go (100%) rename {pkg => go/pkg}/api/provider/provider_test.go (100%) rename {pkg => go/pkg}/api/provider_example_test.go (100%) rename {pkg => go/pkg}/api/provider_test.go (100%) rename {pkg => go/pkg}/api/stdlib_assert_test.go (100%) rename {pkg => go/pkg}/events/events.go (100%) rename {pkg => go/pkg}/events/events_example_test.go (100%) rename {pkg => go/pkg}/events/events_test.go (100%) rename {pkg => go/pkg}/release/changelog.go (100%) rename {pkg => go/pkg}/release/changelog_example_test.go (100%) rename {pkg => go/pkg}/release/changelog_test.go (100%) rename {pkg => go/pkg}/release/config.go (100%) rename {pkg => go/pkg}/release/config_example_test.go (100%) rename {pkg => go/pkg}/release/config_test.go (100%) rename {pkg => go/pkg}/release/output.go (100%) rename {pkg => go/pkg}/release/publishers/assets.go (100%) rename {pkg => go/pkg}/release/publishers/assets_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/assets_test.go (100%) rename {pkg => go/pkg}/release/publishers/aur.go (100%) rename {pkg => go/pkg}/release/publishers/aur_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/aur_test.go (100%) rename {pkg => go/pkg}/release/publishers/chocolatey.go (100%) rename {pkg => go/pkg}/release/publishers/chocolatey_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/chocolatey_test.go (100%) rename {pkg => go/pkg}/release/publishers/docker.go (100%) rename {pkg => go/pkg}/release/publishers/docker_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/docker_test.go (100%) rename {pkg => go/pkg}/release/publishers/github.go (100%) rename {pkg => go/pkg}/release/publishers/github_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/github_test.go (100%) rename {pkg => go/pkg}/release/publishers/homebrew.go (100%) rename {pkg => go/pkg}/release/publishers/homebrew_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/homebrew_test.go (100%) rename {pkg => go/pkg}/release/publishers/integration_test.go (100%) rename {pkg => go/pkg}/release/publishers/linuxkit.go (100%) rename {pkg => go/pkg}/release/publishers/linuxkit_aws.go (100%) rename {pkg => go/pkg}/release/publishers/linuxkit_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/linuxkit_gcp.go (100%) rename {pkg => go/pkg}/release/publishers/linuxkit_iso.go (100%) rename {pkg => go/pkg}/release/publishers/linuxkit_qcow2.go (100%) rename {pkg => go/pkg}/release/publishers/linuxkit_raw.go (100%) rename {pkg => go/pkg}/release/publishers/linuxkit_test.go (100%) rename {pkg => go/pkg}/release/publishers/npm.go (100%) rename {pkg => go/pkg}/release/publishers/npm_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/npm_test.go (100%) rename {pkg => go/pkg}/release/publishers/output.go (100%) rename {pkg => go/pkg}/release/publishers/publisher.go (100%) rename {pkg => go/pkg}/release/publishers/publisher_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/publisher_test.go (100%) rename {pkg => go/pkg}/release/publishers/scoop.go (100%) rename {pkg => go/pkg}/release/publishers/scoop_example_test.go (100%) rename {pkg => go/pkg}/release/publishers/scoop_test.go (100%) rename {pkg => go/pkg}/release/publishers/stdlib_assert_test.go (100%) rename {pkg => go/pkg}/release/publishers/template_funcs.go (100%) rename {pkg => go/pkg}/release/publishers/templates/aur/.SRCINFO.tmpl (100%) rename {pkg => go/pkg}/release/publishers/templates/aur/PKGBUILD.tmpl (100%) rename {pkg => go/pkg}/release/publishers/templates/chocolatey/package.nuspec.tmpl (100%) rename {pkg => go/pkg}/release/publishers/templates/chocolatey/tools/chocolateyinstall.ps1.tmpl (100%) rename {pkg => go/pkg}/release/publishers/templates/homebrew/formula.rb.tmpl (100%) rename {pkg => go/pkg}/release/publishers/templates/npm/install.js.tmpl (100%) rename {pkg => go/pkg}/release/publishers/templates/npm/package.json.tmpl (100%) rename {pkg => go/pkg}/release/publishers/templates/npm/run.js.tmpl (100%) rename {pkg => go/pkg}/release/publishers/templates/scoop/manifest.json.tmpl (100%) rename {pkg => go/pkg}/release/publishers/test_helpers_test.go (100%) rename {pkg => go/pkg}/release/publishers/version_validation_test.go (100%) rename {pkg => go/pkg}/release/release.go (100%) rename {pkg => go/pkg}/release/release_example_test.go (100%) rename {pkg => go/pkg}/release/release_test.go (100%) rename {pkg => go/pkg}/release/sdk.go (100%) rename {pkg => go/pkg}/release/sdk_example_test.go (100%) rename {pkg => go/pkg}/release/sdk_test.go (100%) rename {pkg => go/pkg}/release/stdlib_assert_test.go (100%) rename {pkg => go/pkg}/release/test_helpers_test.go (100%) rename {pkg => go/pkg}/release/version.go (100%) rename {pkg => go/pkg}/release/version_example_test.go (100%) rename {pkg => go/pkg}/release/version_test.go (100%) rename {pkg => go/pkg}/sdk/breaking_test.go (100%) rename {pkg => go/pkg}/sdk/detect.go (100%) rename {pkg => go/pkg}/sdk/detect_example_test.go (100%) rename {pkg => go/pkg}/sdk/detect_test.go (100%) rename {pkg => go/pkg}/sdk/diff.go (100%) rename {pkg => go/pkg}/sdk/diff_example_test.go (100%) rename {pkg => go/pkg}/sdk/diff_test.go (100%) rename {pkg => go/pkg}/sdk/generation_test.go (100%) rename {pkg => go/pkg}/sdk/generators/docker_runtime.go (100%) rename {pkg => go/pkg}/sdk/generators/docker_runtime_test.go (100%) rename {pkg => go/pkg}/sdk/generators/generator.go (100%) rename {pkg => go/pkg}/sdk/generators/generator_example_test.go (100%) rename {pkg => go/pkg}/sdk/generators/generator_test.go (100%) rename {pkg => go/pkg}/sdk/generators/go.go (100%) rename {pkg => go/pkg}/sdk/generators/go_example_test.go (100%) rename {pkg => go/pkg}/sdk/generators/go_test.go (100%) rename {pkg => go/pkg}/sdk/generators/php.go (100%) rename {pkg => go/pkg}/sdk/generators/php_example_test.go (100%) rename {pkg => go/pkg}/sdk/generators/php_test.go (100%) rename {pkg => go/pkg}/sdk/generators/python.go (100%) rename {pkg => go/pkg}/sdk/generators/python_example_test.go (100%) rename {pkg => go/pkg}/sdk/generators/python_test.go (100%) rename {pkg => go/pkg}/sdk/generators/stdlib_assert_test.go (100%) rename {pkg => go/pkg}/sdk/generators/typescript.go (100%) rename {pkg => go/pkg}/sdk/generators/typescript_example_test.go (100%) rename {pkg => go/pkg}/sdk/generators/typescript_test.go (100%) rename {pkg => go/pkg}/sdk/sdk.go (100%) rename {pkg => go/pkg}/sdk/sdk_example_test.go (100%) rename {pkg => go/pkg}/sdk/sdk_test.go (100%) rename {pkg => go/pkg}/sdk/stdlib_assert_test.go (100%) rename {pkg => go/pkg}/sdk/validate.go (100%) rename {pkg => go/pkg}/sdk/validate_example_test.go (100%) rename {pkg => go/pkg}/sdk/validate_test.go (100%) rename {pkg => go/pkg}/service/agentic.go (100%) rename {pkg => go/pkg}/service/agentic_example_test.go (100%) rename {pkg => go/pkg}/service/agentic_test.go (100%) rename {pkg => go/pkg}/service/config.go (100%) rename {pkg => go/pkg}/service/config_example_test.go (100%) rename {pkg => go/pkg}/service/config_test.go (100%) rename {pkg => go/pkg}/service/daemon.go (100%) rename {pkg => go/pkg}/service/daemon_example_test.go (100%) rename {pkg => go/pkg}/service/daemon_run_test.go (100%) rename {pkg => go/pkg}/service/daemon_test.go (100%) rename {pkg => go/pkg}/service/export.go (100%) rename {pkg => go/pkg}/service/export_example_test.go (100%) rename {pkg => go/pkg}/service/export_test.go (100%) rename {pkg => go/pkg}/service/manager.go (100%) rename {pkg => go/pkg}/service/manager_example_test.go (100%) rename {pkg => go/pkg}/service/manager_test.go (100%) rename {pkg => go/pkg}/service/mcp.go (100%) rename {pkg => go/pkg}/service/mcp_test.go (100%) rename {pkg => go/pkg}/service/process_daemon.go (100%) rename {pkg => go/pkg}/service/stdlib_assert_test.go (100%) rename {pkg => go/pkg}/service/test_helpers_test.go (100%) rename {pkg => go/pkg}/storage/storage.go (100%) rename {pkg => go/pkg}/storage/storage_example_test.go (100%) rename {pkg => go/pkg}/storage/storage_test.go (100%) delete mode 100644 pkg/api/ui/dist/core-build.js delete mode 100644 pkg/build/apple.go delete mode 100644 pkg/build/apple/apple.go delete mode 100644 pkg/build/apple/apple_example_test.go delete mode 100644 pkg/build/apple/apple_test.go delete mode 100644 pkg/build/apple_example_test.go delete mode 100644 pkg/build/apple_test.go delete mode 100644 pkg/build/archive.go delete mode 100644 pkg/build/archive_example_test.go delete mode 100644 pkg/build/archive_test.go delete mode 100644 pkg/build/build.go delete mode 100644 pkg/build/build_example_test.go delete mode 100644 pkg/build/build_test.go delete mode 100644 pkg/build/builders/apple.go delete mode 100644 pkg/build/builders/apple_dmg.go delete mode 100644 pkg/build/builders/apple_dmg_example_test.go delete mode 100644 pkg/build/builders/apple_dmg_test.go delete mode 100644 pkg/build/builders/apple_example_test.go delete mode 100644 pkg/build/builders/apple_notarise.go delete mode 100644 pkg/build/builders/apple_notarise_example_test.go delete mode 100644 pkg/build/builders/apple_notarise_test.go delete mode 100644 pkg/build/builders/apple_plist.go delete mode 100644 pkg/build/builders/apple_plist_example_test.go delete mode 100644 pkg/build/builders/apple_plist_test.go delete mode 100644 pkg/build/builders/apple_test.go delete mode 100644 pkg/build/builders/cpp.go delete mode 100644 pkg/build/builders/cpp_example_test.go delete mode 100644 pkg/build/builders/cpp_test.go delete mode 100644 pkg/build/builders/deno.go delete mode 100644 pkg/build/builders/deno_test.go delete mode 100644 pkg/build/builders/docker.go delete mode 100644 pkg/build/builders/docker_example_test.go delete mode 100644 pkg/build/builders/docker_test.go delete mode 100644 pkg/build/builders/docs.go delete mode 100644 pkg/build/builders/docs_example_test.go delete mode 100644 pkg/build/builders/docs_test.go delete mode 100644 pkg/build/builders/env.go delete mode 100644 pkg/build/builders/go.go delete mode 100644 pkg/build/builders/go_example_test.go delete mode 100644 pkg/build/builders/go_test.go delete mode 100644 pkg/build/builders/linuxkit.go delete mode 100644 pkg/build/builders/linuxkit_example_test.go delete mode 100644 pkg/build/builders/linuxkit_image.go delete mode 100644 pkg/build/builders/linuxkit_image_example_test.go delete mode 100644 pkg/build/builders/linuxkit_image_test.go delete mode 100644 pkg/build/builders/linuxkit_test.go delete mode 100644 pkg/build/builders/node.go delete mode 100644 pkg/build/builders/node_example_test.go delete mode 100644 pkg/build/builders/node_test.go delete mode 100644 pkg/build/builders/package_manager.go delete mode 100644 pkg/build/builders/php.go delete mode 100644 pkg/build/builders/php_example_test.go delete mode 100644 pkg/build/builders/php_test.go delete mode 100644 pkg/build/builders/python.go delete mode 100644 pkg/build/builders/python_example_test.go delete mode 100644 pkg/build/builders/python_test.go delete mode 100644 pkg/build/builders/resolver.go delete mode 100644 pkg/build/builders/resolver_example_test.go delete mode 100644 pkg/build/builders/resolver_init_test.go delete mode 100644 pkg/build/builders/resolver_test.go delete mode 100644 pkg/build/builders/rust.go delete mode 100644 pkg/build/builders/rust_example_test.go delete mode 100644 pkg/build/builders/rust_test.go delete mode 100644 pkg/build/builders/taskfile.go delete mode 100644 pkg/build/builders/taskfile_example_test.go delete mode 100644 pkg/build/builders/taskfile_test.go delete mode 100644 pkg/build/builders/wails.go delete mode 100644 pkg/build/builders/wails_example_test.go delete mode 100644 pkg/build/builders/wails_test.go delete mode 100644 pkg/build/builders/zip_deterministic.go delete mode 100644 pkg/build/builtin_resolver.go delete mode 100644 pkg/build/builtin_resolver_example_test.go delete mode 100644 pkg/build/builtin_resolver_test.go delete mode 100644 pkg/build/cache.go delete mode 100644 pkg/build/cache_example_test.go delete mode 100644 pkg/build/cache_test.go delete mode 100644 pkg/build/checksum.go delete mode 100644 pkg/build/checksum_example_test.go delete mode 100644 pkg/build/checksum_test.go delete mode 100644 pkg/build/ci.go delete mode 100644 pkg/build/ci_example_test.go delete mode 100644 pkg/build/ci_test.go delete mode 100644 pkg/build/config.go delete mode 100644 pkg/build/config_example_test.go delete mode 100644 pkg/build/config_test.go delete mode 100644 pkg/build/discovery.go delete mode 100644 pkg/build/discovery_example_test.go delete mode 100644 pkg/build/discovery_test.go delete mode 100644 pkg/build/env.go delete mode 100644 pkg/build/env_example_test.go delete mode 100644 pkg/build/env_test.go delete mode 100644 pkg/build/images/core-dev.yml delete mode 100644 pkg/build/images/core-minimal.yml delete mode 100644 pkg/build/images/core-ml.yml delete mode 100644 pkg/build/installers.go delete mode 100644 pkg/build/installers/installer.go delete mode 100644 pkg/build/installers/installer_example_test.go delete mode 100644 pkg/build/installers/installer_test.go delete mode 100644 pkg/build/installers/templates/agent.sh.tmpl delete mode 100644 pkg/build/installers/templates/ci.sh.tmpl delete mode 100644 pkg/build/installers/templates/dev.sh.tmpl delete mode 100644 pkg/build/installers/templates/go.sh.tmpl delete mode 100644 pkg/build/installers/templates/php.sh.tmpl delete mode 100644 pkg/build/installers/templates/setup.sh.tmpl delete mode 100644 pkg/build/installers_example_test.go delete mode 100644 pkg/build/installers_test.go delete mode 100644 pkg/build/linuxkit_image.go delete mode 100644 pkg/build/linuxkit_image_example_test.go delete mode 100644 pkg/build/linuxkit_image_test.go delete mode 100644 pkg/build/linuxkit_templates.go delete mode 100644 pkg/build/linuxkit_templates_example_test.go delete mode 100644 pkg/build/linuxkit_templates_test.go delete mode 100644 pkg/build/options.go delete mode 100644 pkg/build/options_example_test.go delete mode 100644 pkg/build/options_test.go delete mode 100644 pkg/build/pipeline.go delete mode 100644 pkg/build/pipeline_example_test.go delete mode 100644 pkg/build/pipeline_test.go delete mode 100644 pkg/build/run.go delete mode 100644 pkg/build/run_example_test.go delete mode 100644 pkg/build/run_test.go delete mode 100644 pkg/build/runtime_config.go delete mode 100644 pkg/build/runtime_config_example_test.go delete mode 100644 pkg/build/runtime_config_test.go delete mode 100644 pkg/build/setup.go delete mode 100644 pkg/build/setup_example_test.go delete mode 100644 pkg/build/setup_test.go delete mode 100644 pkg/build/signing/codesign.go delete mode 100644 pkg/build/signing/codesign_example_test.go delete mode 100644 pkg/build/signing/codesign_test.go delete mode 100644 pkg/build/signing/gpg.go delete mode 100644 pkg/build/signing/gpg_example_test.go delete mode 100644 pkg/build/signing/gpg_test.go delete mode 100644 pkg/build/signing/sign.go delete mode 100644 pkg/build/signing/sign_example_test.go delete mode 100644 pkg/build/signing/sign_test.go delete mode 100644 pkg/build/signing/signer.go delete mode 100644 pkg/build/signing/signer_example_test.go delete mode 100644 pkg/build/signing/signer_test.go delete mode 100644 pkg/build/signing/signing_test.go delete mode 100644 pkg/build/signing/signtool.go delete mode 100644 pkg/build/signing/signtool_example_test.go delete mode 100644 pkg/build/signing/signtool_test.go delete mode 100644 pkg/build/templates/release.yml delete mode 100644 pkg/build/testdata/config-project/.core/build.yaml delete mode 100644 pkg/build/testdata/cpp-project/CMakeLists.txt delete mode 100644 pkg/build/testdata/docs-project/mkdocs.yml delete mode 100644 pkg/build/testdata/empty-project/.gitkeep delete mode 100644 pkg/build/testdata/go-project/go.mod delete mode 100644 pkg/build/testdata/monorepo-project/apps/web/package.json delete mode 100644 pkg/build/testdata/multi-project/go.mod delete mode 100644 pkg/build/testdata/multi-project/package.json delete mode 100644 pkg/build/testdata/node-project/package.json delete mode 100644 pkg/build/testdata/php-project/composer.json delete mode 100644 pkg/build/testdata/python-project/pyproject.toml delete mode 100644 pkg/build/testdata/rust-project/Cargo.toml delete mode 100644 pkg/build/testdata/wails-project/go.mod delete mode 100644 pkg/build/testdata/wails-project/wails.json delete mode 100644 pkg/build/version.go delete mode 100644 pkg/build/version_example_test.go delete mode 100644 pkg/build/version_flags.go delete mode 100644 pkg/build/version_flags_example_test.go delete mode 100644 pkg/build/version_flags_test.go delete mode 100644 pkg/build/version_templates.go delete mode 100644 pkg/build/version_templates_example_test.go delete mode 100644 pkg/build/version_templates_test.go delete mode 100644 pkg/build/version_test.go delete mode 100644 pkg/build/workflow.go delete mode 100644 pkg/build/workflow_example_test.go delete mode 100644 pkg/build/workflow_test.go delete mode 100644 pkg/build/xcode_cloud.go delete mode 100644 pkg/build/xcode_cloud_example_test.go delete mode 100644 pkg/build/xcode_cloud_test.go delete mode 100644 pkg/release/testdata/.core/release.yaml delete mode 100644 tests/cli/build/Taskfile.yaml 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/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/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/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 100% rename from cmd/ci/ci.go rename to go/cmd/ci/ci.go 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/cmd/ci/stdlib_assert_test.go b/go/cmd/ci/stdlib_assert_test.go similarity index 100% rename from cmd/ci/stdlib_assert_test.go rename to go/cmd/ci/stdlib_assert_test.go diff --git a/cmd/sdk/cmd.go b/go/cmd/sdk/cmd.go similarity index 100% rename from cmd/sdk/cmd.go rename to go/cmd/sdk/cmd.go 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/cmd/sdk/stdlib_assert_test.go b/go/cmd/sdk/stdlib_assert_test.go similarity index 100% rename from cmd/sdk/stdlib_assert_test.go rename to go/cmd/sdk/stdlib_assert_test.go diff --git a/cmd/service/cmd.go b/go/cmd/service/cmd.go similarity index 100% rename from cmd/service/cmd.go rename to go/cmd/service/cmd.go 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 100% rename from cmd/service/stdlib_assert_test.go rename to go/cmd/service/stdlib_assert_test.go 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 100% rename from internal/cli/cli.go rename to go/internal/cli/cli.go 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 100% rename from internal/cli/cli_test.go rename to go/internal/cli/cli_test.go 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/internal/projectdetect/stdlib_assert_test.go b/go/internal/projectdetect/stdlib_assert_test.go similarity index 100% rename from internal/projectdetect/stdlib_assert_test.go rename to go/internal/projectdetect/stdlib_assert_test.go 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/internal/sdkcfg/stdlib_assert_test.go b/go/internal/sdkcfg/stdlib_assert_test.go similarity index 100% rename from internal/sdkcfg/stdlib_assert_test.go rename to go/internal/sdkcfg/stdlib_assert_test.go 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 100% rename from pkg/api/provider.go rename to go/pkg/api/provider.go 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 100% rename from pkg/api/provider_test.go rename to go/pkg/api/provider_test.go diff --git a/pkg/api/stdlib_assert_test.go b/go/pkg/api/stdlib_assert_test.go similarity index 100% rename from pkg/api/stdlib_assert_test.go rename to go/pkg/api/stdlib_assert_test.go 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/pkg/release/output.go b/go/pkg/release/output.go similarity index 100% rename from pkg/release/output.go rename to go/pkg/release/output.go 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 100% rename from pkg/release/publishers/aur.go rename to go/pkg/release/publishers/aur.go 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 100% rename from pkg/release/publishers/docker_test.go rename to go/pkg/release/publishers/docker_test.go diff --git a/pkg/release/publishers/github.go b/go/pkg/release/publishers/github.go similarity index 100% rename from pkg/release/publishers/github.go rename to go/pkg/release/publishers/github.go 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 100% rename from pkg/release/publishers/github_test.go rename to go/pkg/release/publishers/github_test.go diff --git a/pkg/release/publishers/homebrew.go b/go/pkg/release/publishers/homebrew.go similarity index 100% rename from pkg/release/publishers/homebrew.go rename to go/pkg/release/publishers/homebrew.go 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 100% rename from pkg/release/publishers/linuxkit_test.go rename to go/pkg/release/publishers/linuxkit_test.go diff --git a/pkg/release/publishers/npm.go b/go/pkg/release/publishers/npm.go similarity index 100% rename from pkg/release/publishers/npm.go rename to go/pkg/release/publishers/npm.go 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 100% rename from pkg/release/publishers/scoop.go rename to go/pkg/release/publishers/scoop.go 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 100% rename from pkg/release/publishers/stdlib_assert_test.go rename to go/pkg/release/publishers/stdlib_assert_test.go 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 100% rename from pkg/release/sdk.go rename to go/pkg/release/sdk.go 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 100% rename from pkg/release/stdlib_assert_test.go rename to go/pkg/release/stdlib_assert_test.go 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/pkg/sdk/generators/stdlib_assert_test.go b/go/pkg/sdk/generators/stdlib_assert_test.go similarity index 100% rename from pkg/sdk/generators/stdlib_assert_test.go rename to go/pkg/sdk/generators/stdlib_assert_test.go 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 100% rename from pkg/sdk/stdlib_assert_test.go rename to go/pkg/sdk/stdlib_assert_test.go 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 100% rename from pkg/service/daemon.go rename to go/pkg/service/daemon.go 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 100% rename from pkg/service/mcp_test.go rename to go/pkg/service/mcp_test.go 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 100% rename from pkg/service/stdlib_assert_test.go rename to go/pkg/service/stdlib_assert_test.go 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/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 ? "" : "", n = L; - for (let c = 0; c < s; c++) { - const d = t[c]; - let h, u, f = -1, b = 0; - for (; b < d.length && (n.lastIndex = b, u = n.exec(d), u !== null); ) b = n.lastIndex, n === L ? u[1] === "!--" ? n = be : u[1] !== void 0 ? n = me : u[2] !== void 0 ? (Ae.test(u[2]) && (i = RegExp("" ? (n = i ?? L, f = -1) : u[1] === void 0 ? f = -2 : (f = n.lastIndex - u[2].length, h = u[1], n = u[3] === void 0 ? D : u[3] === '"' ? $e : ve) : n === $e || n === ve ? n = D : n === be || n === me ? n = L : (n = D, i = void 0); - const v = n === D && t[c + 1].startsWith("/>") ? " " : ""; - r += n === L ? d + Ne : f >= 0 ? (a.push(h), d.slice(0, f) + ke + d.slice(f) + P + v) : d + P + (f === -2 ? c : v); - } - return [Se(t, r + (t[s] || "") + (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 Path
- -
-
-
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` -
-
Changelog
-
${this.changelog}
-
- ` : 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.basePath = t.target.value} - /> -
-
- - this.revisionPath = t.target.value} - /> -
- -
- - ${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` -
- Build - -
- -
- ${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/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/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 From 80207d7e9835fc67d5ba25c6b121f91f9b530029 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 30 Apr 2026 13:47:23 +0100 Subject: [PATCH 2/3] chore(lint): clear golangci-lint findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical fixes across cmd/, internal/, pkg/. errcheck wrapping for unchecked returns, removal of unused stdlib_assert aliases (declared but never referenced), staticcheck modernisations. Test files DID get touched in this lane (brief was the older version without the skip-tests rule) — but the changes are safe: deleting unused vars + wrapping ignored returns in `_ =`. No behaviour changes, no test-intent breakage. Pre-existing failure unchanged: - pkg/service TestDaemon_Run_{Good,Bad,Ugly} still panic with gin "handlers already registered for path /api/v1/build/events" — that's a test-isolation bug from the restructure, not from this lane. Tracked separately. --- go.work.sum | 224 ++++++++++++++++++ go/cmd/ci/ci.go | 8 +- go/cmd/ci/stdlib_assert_test.go | 7 +- go/cmd/sdk/cmd.go | 6 +- go/cmd/sdk/stdlib_assert_test.go | 6 +- go/cmd/service/cmd.go | 18 +- go/cmd/service/stdlib_assert_test.go | 4 - go/internal/cli/cli.go | 3 - go/internal/cli/cli_test.go | 2 +- .../projectdetect/stdlib_assert_test.go | 4 - go/internal/sdkcfg/stdlib_assert_test.go | 4 - go/pkg/api/provider.go | 1 - go/pkg/api/provider_test.go | 15 +- go/pkg/api/stdlib_assert_test.go | 3 - go/pkg/release/output.go | 100 -------- go/pkg/release/publishers/aur.go | 2 +- go/pkg/release/publishers/docker_test.go | 11 +- go/pkg/release/publishers/github.go | 4 +- go/pkg/release/publishers/github_test.go | 4 +- go/pkg/release/publishers/homebrew.go | 2 +- go/pkg/release/publishers/linuxkit_test.go | 12 - go/pkg/release/publishers/npm.go | 2 +- go/pkg/release/publishers/scoop.go | 2 +- .../release/publishers/stdlib_assert_test.go | 2 - go/pkg/release/sdk.go | 2 +- go/pkg/release/stdlib_assert_test.go | 1 - go/pkg/sdk/generators/stdlib_assert_test.go | 4 - go/pkg/sdk/stdlib_assert_test.go | 2 - go/pkg/service/daemon.go | 11 +- go/pkg/service/mcp_test.go | 2 +- go/pkg/service/stdlib_assert_test.go | 3 - 31 files changed, 267 insertions(+), 204 deletions(-) create mode 100644 go.work.sum 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/cmd/ci/ci.go b/go/cmd/ci/ci.go index 14c093e..2e5b2da 100644 --- a/go/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/go/cmd/ci/stdlib_assert_test.go b/go/cmd/ci/stdlib_assert_test.go index 3af8c93..b50dec2 100644 --- a/go/cmd/ci/stdlib_assert_test.go +++ b/go/cmd/ci/stdlib_assert_test.go @@ -3,10 +3,5 @@ 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 + stdlibAssertContains = testassert.Contains ) diff --git a/go/cmd/sdk/cmd.go b/go/cmd/sdk/cmd.go index 82a15d8..455e686 100644 --- a/go/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/go/cmd/sdk/stdlib_assert_test.go b/go/cmd/sdk/stdlib_assert_test.go index 45c0fab..77c7103 100644 --- a/go/cmd/sdk/stdlib_assert_test.go +++ b/go/cmd/sdk/stdlib_assert_test.go @@ -4,9 +4,5 @@ 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 + stdlibAssertContains = testassert.Contains ) diff --git a/go/cmd/service/cmd.go b/go/cmd/service/cmd.go index 21e4836..a06f3bb 100644 --- a/go/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/go/cmd/service/stdlib_assert_test.go b/go/cmd/service/stdlib_assert_test.go index 87394da..2e6f5d0 100644 --- a/go/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/internal/cli/cli.go b/go/internal/cli/cli.go index 90c6888..7931f51 100644 --- a/go/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/go/internal/cli/cli_test.go b/go/internal/cli/cli_test.go index 32b000e..e850cd2 100644 --- a/go/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/go/internal/projectdetect/stdlib_assert_test.go b/go/internal/projectdetect/stdlib_assert_test.go index 75055d4..68a69df 100644 --- a/go/internal/projectdetect/stdlib_assert_test.go +++ b/go/internal/projectdetect/stdlib_assert_test.go @@ -4,9 +4,5 @@ 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/go/internal/sdkcfg/stdlib_assert_test.go b/go/internal/sdkcfg/stdlib_assert_test.go index 37d02a6..9c1a08d 100644 --- a/go/internal/sdkcfg/stdlib_assert_test.go +++ b/go/internal/sdkcfg/stdlib_assert_test.go @@ -5,8 +5,4 @@ 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/go/pkg/api/provider.go b/go/pkg/api/provider.go index cf9b48f..01ec3f5 100644 --- a/go/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/go/pkg/api/provider_test.go b/go/pkg/api/provider_test.go index c128bbe..32a9c30 100644 --- a/go/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/go/pkg/api/stdlib_assert_test.go b/go/pkg/api/stdlib_assert_test.go index ea47736..91768ff 100644 --- a/go/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/go/pkg/release/output.go b/go/pkg/release/output.go index 5c5c09a..fcac037 100644 --- a/go/pkg/release/output.go +++ b/go/pkg/release/output.go @@ -1,16 +1,11 @@ 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 @@ -39,42 +34,6 @@ func resolveReleaseOutputRoot(projectDir string, cfg *Config, output coreio.Medi 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) @@ -98,62 +57,3 @@ func mediumEquals(left, right coreio.Medium) bool { 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/go/pkg/release/publishers/aur.go b/go/pkg/release/publishers/aur.go index a083e56..6f371f3 100644 --- a/go/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/go/pkg/release/publishers/docker_test.go b/go/pkg/release/publishers/docker_test.go index 81fbf99..8e1c879 100644 --- a/go/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/go/pkg/release/publishers/github.go b/go/pkg/release/publishers/github.go index edab6bd..555bd5d 100644 --- a/go/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/go/pkg/release/publishers/github_test.go b/go/pkg/release/publishers/github_test.go index 0503195..5d046b8 100644 --- a/go/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/go/pkg/release/publishers/homebrew.go b/go/pkg/release/publishers/homebrew.go index d7334a8..a77e4ea 100644 --- a/go/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/go/pkg/release/publishers/linuxkit_test.go b/go/pkg/release/publishers/linuxkit_test.go index e6c1737..f5cf4fa 100644 --- a/go/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/go/pkg/release/publishers/npm.go b/go/pkg/release/publishers/npm.go index 41cc030..396ecb6 100644 --- a/go/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/go/pkg/release/publishers/scoop.go b/go/pkg/release/publishers/scoop.go index 643544a..67700e8 100644 --- a/go/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/go/pkg/release/publishers/stdlib_assert_test.go b/go/pkg/release/publishers/stdlib_assert_test.go index 15409c8..01b832b 100644 --- a/go/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/go/pkg/release/sdk.go b/go/pkg/release/sdk.go index b292b5e..2b2fa9e 100644 --- a/go/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/go/pkg/release/stdlib_assert_test.go b/go/pkg/release/stdlib_assert_test.go index a4c80f1..46044e6 100644 --- a/go/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/go/pkg/sdk/generators/stdlib_assert_test.go b/go/pkg/sdk/generators/stdlib_assert_test.go index a608d4e..f1e4358 100644 --- a/go/pkg/sdk/generators/stdlib_assert_test.go +++ b/go/pkg/sdk/generators/stdlib_assert_test.go @@ -4,9 +4,5 @@ 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/go/pkg/sdk/stdlib_assert_test.go b/go/pkg/sdk/stdlib_assert_test.go index a6b6330..b6e4520 100644 --- a/go/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/go/pkg/service/daemon.go b/go/pkg/service/daemon.go index c98a5ad..dee8d01 100644 --- a/go/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/go/pkg/service/mcp_test.go b/go/pkg/service/mcp_test.go index ea97143..4a15e02 100644 --- a/go/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/go/pkg/service/stdlib_assert_test.go b/go/pkg/service/stdlib_assert_test.go index 3bb0514..6b909cd 100644 --- a/go/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 { From 956955a48247a1cd6fb6ef5e49159dddaefddd60 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 30 Apr 2026 15:24:26 +0100 Subject: [PATCH 3/3] ci(public): add github actions workflow + README badge block Mirrors api shape: .github/workflows/ci.yml runs test+coverage (Codecov), golangci-lint --tests=false, and sonarcloud-scan-action to dappcore_go-build. README gets the badge block (CI / quality gate / cov / security / maintainability / reliability / smells / NCLOC / pkg.go.dev / license). GOPROXY=direct GOSUMDB=off env in workflow to dodge the proxy.golang.org stale-zip pattern that broke api's first run. Internal Woodpecker pipeline at ci.lthn.sh continues unchanged. --- .github/workflows/ci.yml | 80 ++++++++++++++++++++++++++++++++++++++++ README.md | 16 ++++++++ 2 files changed, 96 insertions(+) create mode 100644 .github/workflows/ci.yml 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/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 + +[![CI](https://github.com/dappcore/go-build/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/dappcore/go-build/actions/workflows/ci.yml) +[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=dappcore_go-build&metric=alert_status)](https://sonarcloud.io/dashboard?id=dappcore_go-build) +[![Coverage](https://codecov.io/gh/dappcore/go-build/branch/dev/graph/badge.svg)](https://codecov.io/gh/dappcore/go-build) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=dappcore_go-build&metric=security_rating)](https://sonarcloud.io/dashboard?id=dappcore_go-build) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dappcore_go-build&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=dappcore_go-build) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=dappcore_go-build&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=dappcore_go-build) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=dappcore_go-build&metric=code_smells)](https://sonarcloud.io/dashboard?id=dappcore_go-build) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=dappcore_go-build&metric=ncloc)](https://sonarcloud.io/dashboard?id=dappcore_go-build) +[![Go Reference](https://pkg.go.dev/badge/dappco.re/go/go-build.svg)](https://pkg.go.dev/dappco.re/go/go-build) +[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](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