From 585c32746e3c84154fe1ba638588557a6bbdfbe4 Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Thu, 20 Aug 2026 10:01:26 +0800 Subject: [PATCH] Cache Go modules in platform builder --- docs/local-debug-workspace.md | 1 + platform/README.md | 1 + platform/api/router.go | 1 + platform/config/config.go | 6 +++++ platform/config/config_test.go | 7 +++-- platform/service/distribution_builder.go | 27 +++++++++++++++---- platform/service/distribution_builder_test.go | 12 ++++++++- scripts/local-debug/env.sh | 1 + scripts/local-debug/smoke.sh | 2 ++ scripts/local-debug/start.sh | 2 ++ 10 files changed, 52 insertions(+), 8 deletions(-) diff --git a/docs/local-debug-workspace.md b/docs/local-debug-workspace.md index e798f33..4a8f2b2 100644 --- a/docs/local-debug-workspace.md +++ b/docs/local-debug-workspace.md @@ -61,6 +61,7 @@ Key platform variables: - `PLATFORM_BUILDER_IMAGE=browser-platform-distribution-builder:1.0.0` - `PLATFORM_BUILDER_SOURCE_DIR=.local-debug/run/build-buckets/source/current` - `PLATFORM_BUILDER_WORKSPACE_DIR=.local-debug/platform/distribution-builds` +- `PLATFORM_BUILDER_CACHE_DIR=.local-debug/platform/distribution-build-cache` - `PLATFORM_BUILDER_TIMEOUT_SECONDS=1800` - `GOCACHE=.local-debug/go-build-cache` diff --git a/platform/README.md b/platform/README.md index 34bf063..5cf03e6 100644 --- a/platform/README.md +++ b/platform/README.md @@ -58,6 +58,7 @@ Runtime configuration: - `PLATFORM_BUILDER_IMAGE`: prebuilt, explicitly versioned or digest-pinned builder image, default `browser-platform-distribution-builder:1.0.0`; floating tags such as `latest` are rejected. - `PLATFORM_BUILDER_SOURCE_DIR`: read-only Run source snapshot containing `go.mod`; this is required for builder readiness. - `PLATFORM_BUILDER_WORKSPACE_DIR`: private per-plugin/per-job build workspace, default `/distribution-builds`. +- `PLATFORM_BUILDER_CACHE_DIR`: persistent Go build/module cache, default `/distribution-build-cache`; it contains no job inputs or component keys. - `PLATFORM_BUILDER_TIMEOUT_SECONDS`: positive build deadline, default `1800`. - `PLATFORM_RUN_RELEASE_URL`: public platform URL embedded into generated components, default `https://scum.npc0.com/`. diff --git a/platform/api/router.go b/platform/api/router.go index 94c1e72..9eac7d2 100644 --- a/platform/api/router.go +++ b/platform/api/router.go @@ -53,6 +53,7 @@ func NewRouterFromConfig(cfg config.Config) (http.Handler, error) { Image: cfg.BuilderImage, SourceDir: cfg.BuilderSourceDir, WorkspaceDir: cfg.BuilderWorkspaceDir, + CacheDir: cfg.BuilderCacheDir, Timeout: time.Duration(cfg.BuilderTimeoutSeconds) * time.Second, })) if err := core.ConfigureAIProviderMode(cfg.AIProviderMode); err != nil { diff --git a/platform/config/config.go b/platform/config/config.go index 04b5aa7..e1d3d4a 100644 --- a/platform/config/config.go +++ b/platform/config/config.go @@ -36,6 +36,7 @@ type Config struct { BuilderImage string BuilderSourceDir string BuilderWorkspaceDir string + BuilderCacheDir string BuilderTimeoutSeconds int } @@ -71,6 +72,10 @@ func Load() Config { if builderWorkspaceDir == "" { builderWorkspaceDir = filepath.Join(dataDir, "distribution-builds") } + builderCacheDir := strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_CACHE_DIR")) + if builderCacheDir == "" { + builderCacheDir = filepath.Join(dataDir, "distribution-build-cache") + } return Config{ Addr: addr, @@ -89,6 +94,7 @@ func Load() Config { BuilderImage: defaultString(strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_IMAGE")), defaultBuilderImage), BuilderSourceDir: strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_SOURCE_DIR")), BuilderWorkspaceDir: builderWorkspaceDir, + BuilderCacheDir: builderCacheDir, BuilderTimeoutSeconds: defaultPositiveInt(strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_TIMEOUT_SECONDS")), defaultBuilderTimeoutSeconds), } } diff --git a/platform/config/config_test.go b/platform/config/config_test.go index f24cbf2..6df88e5 100644 --- a/platform/config/config_test.go +++ b/platform/config/config_test.go @@ -22,6 +22,7 @@ func TestLoadUsesDefaultAddress(t *testing.T) { t.Setenv("PLATFORM_BUILDER_IMAGE", "") t.Setenv("PLATFORM_BUILDER_SOURCE_DIR", "") t.Setenv("PLATFORM_BUILDER_WORKSPACE_DIR", "") + t.Setenv("PLATFORM_BUILDER_CACHE_DIR", "") t.Setenv("PLATFORM_BUILDER_TIMEOUT_SECONDS", "") cfg := Load() @@ -34,7 +35,7 @@ func TestLoadUsesDefaultAddress(t *testing.T) { if cfg.MetadataPath != filepath.Join(".platform-data", "metadata.json") || cfg.LogDir != filepath.Join(".platform-data", "logs") { t.Fatalf("unexpected default storage paths: %+v", cfg) } - if cfg.BuilderDockerBinary != defaultBuilderDockerBinary || cfg.BuilderImage != defaultBuilderImage || cfg.BuilderSourceDir != "" || cfg.BuilderWorkspaceDir != filepath.Join(".platform-data", "distribution-builds") || cfg.BuilderTimeoutSeconds != defaultBuilderTimeoutSeconds { + if cfg.BuilderDockerBinary != defaultBuilderDockerBinary || cfg.BuilderImage != defaultBuilderImage || cfg.BuilderSourceDir != "" || cfg.BuilderWorkspaceDir != filepath.Join(".platform-data", "distribution-builds") || cfg.BuilderCacheDir != filepath.Join(".platform-data", "distribution-build-cache") || cfg.BuilderTimeoutSeconds != defaultBuilderTimeoutSeconds { t.Fatalf("unexpected default builder config: %+v", cfg) } } @@ -54,6 +55,7 @@ func TestLoadUsesConfiguredAddress(t *testing.T) { t.Setenv("PLATFORM_BUILDER_IMAGE", "registry.example.test/distribution-builder:2.0.0") t.Setenv("PLATFORM_BUILDER_SOURCE_DIR", "/srv/run-source") t.Setenv("PLATFORM_BUILDER_WORKSPACE_DIR", "/srv/distribution-builds") + t.Setenv("PLATFORM_BUILDER_CACHE_DIR", "/srv/distribution-build-cache") t.Setenv("PLATFORM_BUILDER_TIMEOUT_SECONDS", "900") cfg := Load() @@ -63,7 +65,7 @@ func TestLoadUsesConfiguredAddress(t *testing.T) { if cfg.StorageBackend != "mysql" || cfg.MySQLDSN != "platform:platform@tcp(127.0.0.1:3306)/platform?parseTime=true" || cfg.DataDir != "/tmp/platform-data" || cfg.MetadataPath != "/tmp/platform-metadata.json" || cfg.LogDir != "/tmp/platform-logs" || cfg.LogBodyBackend != "file" || cfg.BootstrapAdminEmail != "admin@example.test" || cfg.BootstrapAdminPassword != "configured-secret" || cfg.SecretEnvelopeKey != "configured-envelope-key-at-least-32-bytes" { t.Fatalf("unexpected configured storage: %+v", cfg) } - if cfg.BuilderDockerBinary != "/usr/local/bin/docker" || cfg.BuilderImage != "registry.example.test/distribution-builder:2.0.0" || cfg.BuilderSourceDir != "/srv/run-source" || cfg.BuilderWorkspaceDir != "/srv/distribution-builds" || cfg.BuilderTimeoutSeconds != 900 { + if cfg.BuilderDockerBinary != "/usr/local/bin/docker" || cfg.BuilderImage != "registry.example.test/distribution-builder:2.0.0" || cfg.BuilderSourceDir != "/srv/run-source" || cfg.BuilderWorkspaceDir != "/srv/distribution-builds" || cfg.BuilderCacheDir != "/srv/distribution-build-cache" || cfg.BuilderTimeoutSeconds != 900 { t.Fatalf("unexpected configured builder: %+v", cfg) } } @@ -126,6 +128,7 @@ func clearPlatformEnv(t *testing.T) { "PLATFORM_BUILDER_IMAGE", "PLATFORM_BUILDER_SOURCE_DIR", "PLATFORM_BUILDER_WORKSPACE_DIR", + "PLATFORM_BUILDER_CACHE_DIR", "PLATFORM_BUILDER_TIMEOUT_SECONDS", } { t.Setenv(key, "") diff --git a/platform/service/distribution_builder.go b/platform/service/distribution_builder.go index 1450a93..2d998dd 100644 --- a/platform/service/distribution_builder.go +++ b/platform/service/distribution_builder.go @@ -52,6 +52,7 @@ type DockerDistributionBuilderConfig struct { Image string SourceDir string WorkspaceDir string + CacheDir string Timeout time.Duration PlatformURL string CommandRunner func(ctx context.Context, name string, args ...string) ([]byte, error) @@ -198,6 +199,10 @@ func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.Distrib if err != nil { return nil, validationError("platform builder workspace directory is invalid") } + cacheDir, err := builder.cacheDir(workspaceDir) + if err != nil { + return nil, validationError("platform builder cache directory is invalid") + } // Workspaces stay isolated per plugin and per job as required by // run-build-download-flow. jobDir := filepath.Join(workspaceDir, sanitizeIDPart(input.PluginID), sanitizeIDPart(input.JobID)) @@ -207,7 +212,9 @@ func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.Distrib outputDir := filepath.Join(jobDir, "output") inputDir := filepath.Join(jobDir, "input") buildDir := filepath.Join(jobDir, "build") - for _, directory := range []string{outputDir, inputDir, buildDir} { + goBuildCacheDir := filepath.Join(cacheDir, "go-build") + goModCacheDir := filepath.Join(cacheDir, "go-mod") + for _, directory := range []string{outputDir, inputDir, buildDir, goBuildCacheDir, goModCacheDir} { if err := os.MkdirAll(directory, 0o700); err != nil { return nil, err } @@ -256,7 +263,7 @@ func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.Distrib } ctx, cancel := context.WithTimeout(context.Background(), builder.config.Timeout) defer cancel() - args := builder.containerArgs(input, sourceDir, inputDir, buildDir, outputDir, outputName) + args := builder.containerArgs(input, sourceDir, inputDir, buildDir, outputDir, goBuildCacheDir, goModCacheDir, outputName) reportBuilderProgress(progress, 18, "git_sync: platform builder container starting") output, err := builder.runBuildCommand(ctx, args, progress) if err != nil { @@ -283,6 +290,14 @@ func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.Distrib return packageClientManagerDistribution(input.PackageFormat, outputName, binary, configPayload) } +func (builder *DockerDistributionBuilder) cacheDir(workspaceDir string) (string, error) { + configured := strings.TrimSpace(builder.config.CacheDir) + if configured == "" { + configured = filepath.Join(workspaceDir, "_cache") + } + return filepath.Abs(configured) +} + func (builder *DockerDistributionBuilder) runBuildCommand(ctx context.Context, args []string, progress func(DistributionBuildProgress)) ([]byte, error) { if builder.config.CommandStream == nil { return builder.config.CommandRunner(ctx, builder.config.DockerBinary, args...) @@ -410,7 +425,7 @@ cp config.yaml /workspace/output/config.yaml progress 88 'package_finalize: client-manager package inputs written' ` -func (builder *DockerDistributionBuilder) containerArgs(input domain.DistributionBuildInput, sourceDir string, inputDir string, buildDir string, outputDir string, outputName string) []string { +func (builder *DockerDistributionBuilder) containerArgs(input domain.DistributionBuildInput, sourceDir string, inputDir string, buildDir string, outputDir string, goBuildCacheDir string, goModCacheDir string, outputName string) []string { platformURL := strings.TrimSpace(input.PlatformURL) if platformURL == "" { platformURL = strings.TrimSpace(builder.config.PlatformURL) @@ -424,11 +439,13 @@ func (builder *DockerDistributionBuilder) containerArgs(input domain.Distributio "-v", inputDir + ":/workspace/input:ro", "-v", buildDir + ":/workspace/build", "-v", outputDir + ":/workspace/output", + "-v", goBuildCacheDir + ":/workspace/cache/go-build", + "-v", goModCacheDir + ":/workspace/cache/go-mod", "-e", "CGO_ENABLED=0", "-e", "GOOS=" + input.TargetOS, "-e", "GOARCH=" + input.TargetArch, - "-e", "GOCACHE=/tmp/go-build", - "-e", "GOMODCACHE=/tmp/go-mod", + "-e", "GOCACHE=/workspace/cache/go-build", + "-e", "GOMODCACHE=/workspace/cache/go-mod", "-e", "COMPONENT_KIND=" + string(input.ComponentKind), "-e", "SERVER_INSTANCE_ID=" + input.ServerInstanceID, "-e", "PLUGIN_ID=" + input.PluginID, diff --git a/platform/service/distribution_builder_test.go b/platform/service/distribution_builder_test.go index 5bbadd4..dcc7451 100644 --- a/platform/service/distribution_builder_test.go +++ b/platform/service/distribution_builder_test.go @@ -97,6 +97,7 @@ func TestDockerDistributionBuilderReadinessNamesPlatformBuilderFailures(t *testi func TestDockerDistributionBuilderKeepsSecretInIsolatedInput(t *testing.T) { sourceDir := createBuilderSource(t) workspaceDir := t.TempDir() + cacheDir := filepath.Join(t.TempDir(), "builder-cache") secret := "component-auth-key-that-must-not-leave-input" var dockerArgs []string builder := NewDockerDistributionBuilder(DockerDistributionBuilderConfig{ @@ -104,6 +105,7 @@ func TestDockerDistributionBuilderKeepsSecretInIsolatedInput(t *testing.T) { Image: "browser-platform-distribution-builder:1.0.0", SourceDir: sourceDir, WorkspaceDir: workspaceDir, + CacheDir: cacheDir, CommandRunner: func(_ context.Context, name string, args ...string) ([]byte, error) { if name != "docker-test" { t.Fatalf("unexpected container runtime %q", name) @@ -114,6 +116,11 @@ func TestDockerDistributionBuilderKeepsSecretInIsolatedInput(t *testing.T) { dockerArgs = append([]string(nil), args...) inputDir := builderMountHostPath(t, args, "/workspace/input:ro") outputDir := builderMountHostPath(t, args, "/workspace/output") + goBuildCacheDir := builderMountHostPath(t, args, "/workspace/cache/go-build") + goModCacheDir := builderMountHostPath(t, args, "/workspace/cache/go-mod") + if goBuildCacheDir != filepath.Join(cacheDir, "go-build") || goModCacheDir != filepath.Join(cacheDir, "go-mod") { + t.Fatalf("unexpected Go cache mounts: build=%q mod=%q", goBuildCacheDir, goModCacheDir) + } authPath := filepath.Join(inputDir, "auth-key") payload, err := os.ReadFile(authPath) if err != nil { @@ -174,7 +181,7 @@ func TestDockerDistributionBuilderKeepsSecretInIsolatedInput(t *testing.T) { if strings.Contains(joinedArgs, secret) { t.Fatal("component auth key leaked into Docker arguments or environment") } - for _, required := range []string{"--read-only", "--pull\x00never", sourceDir + ":/workspace/source:ro", ":/workspace/input:ro", ":/workspace/output"} { + for _, required := range []string{"--read-only", "--pull\x00never", sourceDir + ":/workspace/source:ro", ":/workspace/input:ro", ":/workspace/output", cacheDir + "/go-build:/workspace/cache/go-build", cacheDir + "/go-mod:/workspace/cache/go-mod", "GOCACHE=/workspace/cache/go-build", "GOMODCACHE=/workspace/cache/go-mod"} { if !strings.Contains(joinedArgs, required) { t.Fatalf("Docker arguments do not contain required isolation %q: %q", required, joinedArgs) } @@ -183,6 +190,9 @@ func TestDockerDistributionBuilderKeepsSecretInIsolatedInput(t *testing.T) { if _, err := os.Stat(jobDir); !errors.Is(err, os.ErrNotExist) { t.Fatalf("per-job workspace was not removed: %v", err) } + if _, err := os.Stat(filepath.Join(cacheDir, "go-mod")); err != nil { + t.Fatalf("persistent Go module cache was not retained: %v", err) + } } func TestDockerDistributionBuilderStreamsProgressMarkers(t *testing.T) { diff --git a/scripts/local-debug/env.sh b/scripts/local-debug/env.sh index 0dad325..f37223a 100755 --- a/scripts/local-debug/env.sh +++ b/scripts/local-debug/env.sh @@ -43,6 +43,7 @@ export PLATFORM_BUILDER_DOCKER_BINARY="${PLATFORM_BUILDER_DOCKER_BINARY:-docker} export PLATFORM_BUILDER_IMAGE="${PLATFORM_BUILDER_IMAGE:-browser-platform-distribution-builder:1.0.0}" export PLATFORM_BUILDER_SOURCE_DIR="${PLATFORM_BUILDER_SOURCE_DIR:-$RUN_BUILD_SOURCE_ROOT}" export PLATFORM_BUILDER_WORKSPACE_DIR="${PLATFORM_BUILDER_WORKSPACE_DIR:-$PLATFORM_DATA_DIR/distribution-builds}" +export PLATFORM_BUILDER_CACHE_DIR="${PLATFORM_BUILDER_CACHE_DIR:-$PLATFORM_DATA_DIR/distribution-build-cache}" export PLATFORM_BUILDER_TIMEOUT_SECONDS="${PLATFORM_BUILDER_TIMEOUT_SECONDS:-1800}" export RUN_SPOOL_ROOT="${RUN_SPOOL_ROOT:-$LOCAL_DEBUG_ROOT/run/spool}" export RUN_MAX_JOBS="${RUN_MAX_JOBS:-1}" diff --git a/scripts/local-debug/smoke.sh b/scripts/local-debug/smoke.sh index 013c016..a5a857c 100755 --- a/scripts/local-debug/smoke.sh +++ b/scripts/local-debug/smoke.sh @@ -47,6 +47,7 @@ PLATFORM_BUILDER_DOCKER_BINARY=$PLATFORM_BUILDER_DOCKER_BINARY PLATFORM_BUILDER_IMAGE=$PLATFORM_BUILDER_IMAGE PLATFORM_BUILDER_SOURCE_DIR=$PLATFORM_BUILDER_SOURCE_DIR PLATFORM_BUILDER_WORKSPACE_DIR=$PLATFORM_BUILDER_WORKSPACE_DIR +PLATFORM_BUILDER_CACHE_DIR=$PLATFORM_BUILDER_CACHE_DIR PLATFORM_BUILDER_TIMEOUT_SECONDS=$PLATFORM_BUILDER_TIMEOUT_SECONDS RUN_WORKSPACE_ROOT=$RUN_WORKSPACE_ROOT RUN_SPOOL_ROOT=$RUN_SPOOL_ROOT @@ -277,6 +278,7 @@ start_self_hosted_stack() { PLATFORM_BUILDER_IMAGE="$PLATFORM_BUILDER_IMAGE" \ PLATFORM_BUILDER_SOURCE_DIR="$PLATFORM_BUILDER_SOURCE_DIR" \ PLATFORM_BUILDER_WORKSPACE_DIR="$PLATFORM_BUILDER_WORKSPACE_DIR" \ + PLATFORM_BUILDER_CACHE_DIR="$PLATFORM_BUILDER_CACHE_DIR" \ PLATFORM_BUILDER_TIMEOUT_SECONDS="$PLATFORM_BUILDER_TIMEOUT_SECONDS" \ go run ./cmd/platform ) >"$LOCAL_DEBUG_LOG_DIR/platform.log" 2>&1 & diff --git a/scripts/local-debug/start.sh b/scripts/local-debug/start.sh index d1e2827..fa50eeb 100755 --- a/scripts/local-debug/start.sh +++ b/scripts/local-debug/start.sh @@ -245,6 +245,7 @@ printf 'run build source snapshot: %s\n' "$RUN_BUILD_SOURCE_ROOT" printf 'run bootstrap binary: %s\n' "$RUN_BOOTSTRAP_BIN" printf 'platform builder image: %s\n' "$PLATFORM_BUILDER_IMAGE" printf 'platform builder workspace: %s\n' "$PLATFORM_BUILDER_WORKSPACE_DIR" +printf 'platform builder cache: %s\n' "$PLATFORM_BUILDER_CACHE_DIR" printf 'platform log: %s\n' "$LOCAL_DEBUG_LOG_DIR/platform.log" printf 'run log: %s\n' "$LOCAL_DEBUG_LOG_DIR/run.log" printf 'platform_web log: %s\n' "$LOCAL_DEBUG_LOG_DIR/platform_web.log" @@ -269,6 +270,7 @@ start_service platform "$ROOT_DIR/platform" env \ PLATFORM_BUILDER_IMAGE="$PLATFORM_BUILDER_IMAGE" \ PLATFORM_BUILDER_SOURCE_DIR="$PLATFORM_BUILDER_SOURCE_DIR" \ PLATFORM_BUILDER_WORKSPACE_DIR="$PLATFORM_BUILDER_WORKSPACE_DIR" \ + PLATFORM_BUILDER_CACHE_DIR="$PLATFORM_BUILDER_CACHE_DIR" \ PLATFORM_BUILDER_TIMEOUT_SECONDS="$PLATFORM_BUILDER_TIMEOUT_SECONDS" \ go run ./cmd/platform