diff --git a/README.md b/README.md index 3b98119..df6be93 100644 --- a/README.md +++ b/README.md @@ -44,16 +44,16 @@ By default, `gobuildcache` uses an on-disk cache stored in the OS default tempor For "production" use-cases in CI, you'll want to configure `gobuildcache` to use S3 Express One Zone, or a similarly low latency distributed backend. ```bash -export BACKEND_TYPE=s3 -export S3_BUCKET=$BUCKET_NAME +export GOBUILDCACHE_BACKEND_TYPE=s3 +export GOBUILDCACHE_S3_BUCKET=$BUCKET_NAME ``` You'll also have to provide AWS credentials. `gobuildcache` embeds the AWS V2 S3 SDK so any method of providing credentials to that library will work, but the simplest is to use environment variables as demonstrated below. ```bash export GOCACHEPROG=gobuildcache -export BACKEND_TYPE=s3 -export S3_BUCKET=$BUCKET_NAME +export GOBUILDCACHE_BACKEND_TYPE=s3 +export GOBUILDCACHE_S3_BUCKET=$BUCKET_NAME export AWS_REGION=$BUCKET_REGION export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY @@ -61,6 +61,8 @@ go build ./... go test ./... ``` +> **Note**: All configuration environment variables support both `GOBUILDCACHE_` and `` forms (e.g., both `GOBUILDCACHE_S3_BUCKET` and `S3_BUCKET` work). The prefixed version takes precedence if both are set. The prefixed form is recommended to avoid conflicts with other tools. If the prefixed variable is set to an empty string, it falls through to the unprefixed version (or default). + Your credentials must have the following permissions: ```json @@ -145,16 +147,18 @@ The clear commands take the same flags / environment variables as the regular `g `gobuildcache` ships with reasonable defaults, but this section provides a complete overview of flags / environment variables that can be used to override behavior. +All environment variables support both `GOBUILDCACHE_` and `` forms (e.g., `GOBUILDCACHE_S3_BUCKET` or `S3_BUCKET`). The prefixed version takes precedence if both are set. + | Flag | Environment Variable | Default | Description | -|------|---------------------|---------|-------------| -| `-backend` | `BACKEND_TYPE` | `disk` | Backend type: `disk` or `s3` | -| `-lock-type` | `LOCK_TYPE` | `fslock` | Mechanism for locking: `fslock` (filesystem) or `memory` | -| `-cache-dir` | `CACHE_DIR` | `/$OS_TMP/gobuildcache/cache` | Local cache directory | -| `-lock-dir` | `LOCK_DIR` | `/$OS_TMP/gobuildcache/locks` | Local directory for storing filesystem locks | -| `-s3-bucket` | `S3_BUCKET` | (none) | S3 bucket name (required for S3) | -| `-s3-prefix` | `S3_PREFIX` | (empty) | S3 key prefix | -| `-debug` | `DEBUG` | `false` | Enable debug logging | -| `-stats` | `PRINT_STATS` | `false` | Print cache statistics on exit | +|------|----------------------|---------|-------------| +| `-backend` | `GOBUILDCACHE_BACKEND_TYPE` | `disk` | Backend type: `disk` or `s3` | +| `-lock-type` | `GOBUILDCACHE_LOCK_TYPE` | `fslock` | Locking: `fslock` or `memory` | +| `-cache-dir` | `GOBUILDCACHE_CACHE_DIR` | `$TMPDIR/gobuildcache/cache` | Local cache directory | +| `-lock-dir` | `GOBUILDCACHE_LOCK_DIR` | `$TMPDIR/gobuildcache/locks` | Filesystem lock directory | +| `-s3-bucket` | `GOBUILDCACHE_S3_BUCKET` | (none) | S3 bucket name (required for S3) | +| `-s3-prefix` | `GOBUILDCACHE_S3_PREFIX` | (empty) | S3 key prefix | +| `-debug` | `GOBUILDCACHE_DEBUG` | `false` | Enable debug logging | +| `-stats` | `GOBUILDCACHE_PRINT_STATS` | `false` | Print cache statistics on exit | # How it Works diff --git a/env_test.go b/env_test.go new file mode 100644 index 0000000..53416da --- /dev/null +++ b/env_test.go @@ -0,0 +1,284 @@ +package main + +import ( + "testing" +) + +func TestGetEnvWithPrefix(t *testing.T) { + tests := []struct { + name string + key string + defaultValue string + envVars map[string]string + expected string + }{ + { + name: "returns default when neither env var is set", + key: "TEST_KEY", + defaultValue: "default", + envVars: map[string]string{}, + expected: "default", + }, + { + name: "returns unprefixed value when only unprefixed is set", + key: "TEST_KEY", + defaultValue: "default", + envVars: map[string]string{"TEST_KEY": "unprefixed_value"}, + expected: "unprefixed_value", + }, + { + name: "returns prefixed value when only prefixed is set", + key: "TEST_KEY", + defaultValue: "default", + envVars: map[string]string{"GOBUILDCACHE_TEST_KEY": "prefixed_value"}, + expected: "prefixed_value", + }, + { + name: "prefixed value takes precedence over unprefixed", + key: "TEST_KEY", + defaultValue: "default", + envVars: map[string]string{ + "TEST_KEY": "unprefixed_value", + "GOBUILDCACHE_TEST_KEY": "prefixed_value", + }, + expected: "prefixed_value", + }, + { + name: "works with S3_BUCKET style keys", + key: "S3_BUCKET", + defaultValue: "", + envVars: map[string]string{"GOBUILDCACHE_S3_BUCKET": "my-bucket"}, + expected: "my-bucket", + }, + { + name: "works with BACKEND_TYPE style keys", + key: "BACKEND_TYPE", + defaultValue: "disk", + envVars: map[string]string{"GOBUILDCACHE_BACKEND_TYPE": "s3"}, + expected: "s3", + }, + { + name: "empty prefixed value falls through to unprefixed", + key: "TEST_KEY", + defaultValue: "default", + envVars: map[string]string{ + "TEST_KEY": "unprefixed_value", + "GOBUILDCACHE_TEST_KEY": "", + }, + expected: "unprefixed_value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear environment variables (t.Setenv auto-restores on test completion) + t.Setenv(tt.key, "") + t.Setenv("GOBUILDCACHE_"+tt.key, "") + // Set test-specific environment variables + for k, v := range tt.envVars { + t.Setenv(k, v) + } + + result := getEnvWithPrefix(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("getEnvWithPrefix(%q, %q) = %q, want %q", tt.key, tt.defaultValue, result, tt.expected) + } + }) + } +} + +func TestGetEnvBoolWithPrefix(t *testing.T) { + tests := []struct { + name string + key string + defaultValue bool + envVars map[string]string + expected bool + }{ + { + name: "returns default when neither env var is set", + key: "TEST_BOOL", + defaultValue: false, + envVars: map[string]string{}, + expected: false, + }, + { + name: "returns true default when neither env var is set", + key: "TEST_BOOL", + defaultValue: true, + envVars: map[string]string{}, + expected: true, + }, + { + name: "returns unprefixed value when only unprefixed is set", + key: "TEST_BOOL", + defaultValue: false, + envVars: map[string]string{"TEST_BOOL": "true"}, + expected: true, + }, + { + name: "returns prefixed value when only prefixed is set", + key: "TEST_BOOL", + defaultValue: false, + envVars: map[string]string{"GOBUILDCACHE_TEST_BOOL": "true"}, + expected: true, + }, + { + name: "prefixed value takes precedence over unprefixed", + key: "TEST_BOOL", + defaultValue: false, + envVars: map[string]string{ + "TEST_BOOL": "false", + "GOBUILDCACHE_TEST_BOOL": "true", + }, + expected: true, + }, + { + name: "prefixed false overrides unprefixed true", + key: "TEST_BOOL", + defaultValue: true, + envVars: map[string]string{ + "TEST_BOOL": "true", + "GOBUILDCACHE_TEST_BOOL": "false", + }, + expected: false, + }, + { + name: "accepts 1 as true", + key: "TEST_BOOL", + defaultValue: false, + envVars: map[string]string{"GOBUILDCACHE_TEST_BOOL": "1"}, + expected: true, + }, + { + name: "accepts yes as true", + key: "TEST_BOOL", + defaultValue: false, + envVars: map[string]string{"GOBUILDCACHE_TEST_BOOL": "yes"}, + expected: true, + }, + { + name: "accepts YES as true (case insensitive)", + key: "TEST_BOOL", + defaultValue: false, + envVars: map[string]string{"GOBUILDCACHE_TEST_BOOL": "YES"}, + expected: true, + }, + { + name: "empty prefixed value falls through to unprefixed", + key: "TEST_BOOL", + defaultValue: false, + envVars: map[string]string{ + "TEST_BOOL": "true", + "GOBUILDCACHE_TEST_BOOL": "", + }, + expected: true, + }, + { + name: "invalid prefixed value falls through to unprefixed", + key: "TEST_BOOL", + defaultValue: false, + envVars: map[string]string{ + "TEST_BOOL": "true", + "GOBUILDCACHE_TEST_BOOL": "not-a-bool", + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear environment variables (t.Setenv auto-restores on test completion) + t.Setenv(tt.key, "") + t.Setenv("GOBUILDCACHE_"+tt.key, "") + // Set test-specific environment variables + for k, v := range tt.envVars { + t.Setenv(k, v) + } + + result := getEnvBoolWithPrefix(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("getEnvBoolWithPrefix(%q, %v) = %v, want %v", tt.key, tt.defaultValue, result, tt.expected) + } + }) + } +} + +func TestGetEnvFloatWithPrefix(t *testing.T) { + tests := []struct { + name string + key string + defaultValue float64 + envVars map[string]string + expected float64 + }{ + { + name: "returns default when neither env var is set", + key: "TEST_FLOAT", + defaultValue: 0.5, + envVars: map[string]string{}, + expected: 0.5, + }, + { + name: "returns unprefixed value when only unprefixed is set", + key: "TEST_FLOAT", + defaultValue: 0.0, + envVars: map[string]string{"TEST_FLOAT": "0.75"}, + expected: 0.75, + }, + { + name: "returns prefixed value when only prefixed is set", + key: "TEST_FLOAT", + defaultValue: 0.0, + envVars: map[string]string{"GOBUILDCACHE_TEST_FLOAT": "0.25"}, + expected: 0.25, + }, + { + name: "prefixed value takes precedence over unprefixed", + key: "TEST_FLOAT", + defaultValue: 0.0, + envVars: map[string]string{ + "TEST_FLOAT": "0.5", + "GOBUILDCACHE_TEST_FLOAT": "0.9", + }, + expected: 0.9, + }, + { + name: "returns default for invalid prefixed value, falls back to unprefixed", + key: "TEST_FLOAT", + defaultValue: 0.0, + envVars: map[string]string{ + "TEST_FLOAT": "0.5", + "GOBUILDCACHE_TEST_FLOAT": "not-a-number", + }, + expected: 0.5, + }, + { + name: "empty prefixed value falls through to unprefixed", + key: "TEST_FLOAT", + defaultValue: 0.0, + envVars: map[string]string{ + "TEST_FLOAT": "0.75", + "GOBUILDCACHE_TEST_FLOAT": "", + }, + expected: 0.75, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear environment variables (t.Setenv auto-restores on test completion) + t.Setenv(tt.key, "") + t.Setenv("GOBUILDCACHE_"+tt.key, "") + // Set test-specific environment variables + for k, v := range tt.envVars { + t.Setenv(k, v) + } + + result := getEnvFloatWithPrefix(tt.key, tt.defaultValue) + if result != tt.expected { + t.Errorf("getEnvFloatWithPrefix(%q, %v) = %v, want %v", tt.key, tt.defaultValue, result, tt.expected) + } + }) + } +} diff --git a/examples/github_actions_s3.yml b/examples/github_actions_s3.yml index 288f852..9c36d4a 100644 --- a/examples/github_actions_s3.yml +++ b/examples/github_actions_s3.yml @@ -24,9 +24,9 @@ jobs: run: go install github.com/richardartoul/gobuildcache@latest - name: Run short tests env: - BACKEND_TYPE: s3 - S3_BUCKET: EXAMPLE_BUCKET - S3_PREFIX: EXAMPLE_PREFIX + GOBUILDCACHE_BACKEND_TYPE: s3 + GOBUILDCACHE_S3_BUCKET: EXAMPLE_BUCKET + GOBUILDCACHE_S3_PREFIX: EXAMPLE_PREFIX AWS_REGION: EXAMPLE_REGION AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/examples/github_actions_tigris.yml b/examples/github_actions_tigris.yml index 9ed7536..3b09c39 100644 --- a/examples/github_actions_tigris.yml +++ b/examples/github_actions_tigris.yml @@ -24,14 +24,13 @@ jobs: run: go install github.com/richardartoul/gobuildcache@latest - name: Run short tests env: - BACKEND_TYPE: s3 - S3_BUCKET: EXAMPLE_BUCKET - S3_PREFIX: EXAMPLE_PREFIX - AWS_REGION: EXAMPLE_REGION + GOBUILDCACHE_BACKEND_TYPE: s3 + GOBUILDCACHE_S3_BUCKET: EXAMPLE_BUCKET + GOBUILDCACHE_S3_PREFIX: EXAMPLE_PREFIX + AWS_REGION: auto AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ENDPOINT_URL_S3: https://t3.storage.dev AWS_ENDPOINT_URL_IAM: https://iam.storage.dev - AWS_REGION: auto GOCACHEPROG: gobuildcache run: go test ./... diff --git a/integrationtests/integration_clear_test.go b/integrationtests/integration_clear_test.go index 2cd30b4..3c9b713 100644 --- a/integrationtests/integration_clear_test.go +++ b/integrationtests/integration_clear_test.go @@ -53,9 +53,9 @@ func TestCacheClear(t *testing.T) { firstRunCmd.Dir = workspaceDir firstRunCmd.Env = append(os.Environ(), "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=disk", - "DEBUG=false", // Less verbose - "CACHE_DIR="+cacheDir) + "GOBUILDCACHE_BACKEND_TYPE=disk", + "GOBUILDCACHE_DEBUG=false", + "GOBUILDCACHE_CACHE_DIR="+cacheDir) var firstRunOutput bytes.Buffer firstRunCmd.Stdout = &firstRunOutput @@ -83,9 +83,9 @@ func TestCacheClear(t *testing.T) { secondRunCmd.Dir = workspaceDir secondRunCmd.Env = append(os.Environ(), "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=disk", - "DEBUG=false", - "CACHE_DIR="+cacheDir) + "GOBUILDCACHE_BACKEND_TYPE=disk", + "GOBUILDCACHE_DEBUG=false", + "GOBUILDCACHE_CACHE_DIR="+cacheDir) var secondRunOutput bytes.Buffer secondRunCmd.Stdout = &secondRunOutput @@ -148,9 +148,9 @@ func TestCacheClear(t *testing.T) { thirdRunCmd.Dir = workspaceDir thirdRunCmd.Env = append(os.Environ(), "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=disk", - "DEBUG=false", - "CACHE_DIR="+cacheDir) + "GOBUILDCACHE_BACKEND_TYPE=disk", + "GOBUILDCACHE_DEBUG=false", + "GOBUILDCACHE_CACHE_DIR="+cacheDir) var thirdRunOutput bytes.Buffer thirdRunCmd.Stdout = &thirdRunOutput diff --git a/integrationtests/integration_concurrent_test.go b/integrationtests/integration_concurrent_test.go index a97c747..fe01814 100644 --- a/integrationtests/integration_concurrent_test.go +++ b/integrationtests/integration_concurrent_test.go @@ -84,12 +84,12 @@ func TestCacheIntegrationConcurrentProcesses(t *testing.T) { // Use fslock for cross-process deduplication cmd.Env = append(os.Environ(), "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=disk", - "DEBUG=false", - "PRINT_STATS=true", - "LOCK_TYPE=fslock", - "LOCK_DIR="+lockDir, - "CACHE_DIR="+cacheDir) + "GOBUILDCACHE_BACKEND_TYPE=disk", + "GOBUILDCACHE_DEBUG=false", + "GOBUILDCACHE_PRINT_STATS=true", + "GOBUILDCACHE_LOCK_TYPE=fslock", + "GOBUILDCACHE_LOCK_DIR="+lockDir, + "GOBUILDCACHE_CACHE_DIR="+cacheDir) cmd.Stdout = &outputs[index] cmd.Stderr = &outputs[index] @@ -126,12 +126,12 @@ func TestCacheIntegrationConcurrentProcesses(t *testing.T) { secondCmd.Dir = workspaceDir secondCmd.Env = append(os.Environ(), "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=disk", - "DEBUG=false", - "PRINT_STATS=true", - "LOCK=fslock", - "LOCK_DIR="+lockDir, - "CACHE_DIR="+cacheDir) + "GOBUILDCACHE_BACKEND_TYPE=disk", + "GOBUILDCACHE_DEBUG=false", + "GOBUILDCACHE_PRINT_STATS=true", + "GOBUILDCACHE_LOCK_TYPE=fslock", + "GOBUILDCACHE_LOCK_DIR="+lockDir, + "GOBUILDCACHE_CACHE_DIR="+cacheDir) secondCmd.Stdout = &secondBatchOutput secondCmd.Stderr = &secondBatchOutput diff --git a/integrationtests/integration_error_test.go b/integrationtests/integration_error_test.go index 9078bbc..4615133 100644 --- a/integrationtests/integration_error_test.go +++ b/integrationtests/integration_error_test.go @@ -57,10 +57,10 @@ func TestCacheIntegrationErrorBackend(t *testing.T) { testCmd.Dir = workspaceDir testCmd.Env = append(os.Environ(), "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=disk", - "CACHE_DIR="+cacheDir, - "ERROR_RATE="+fmt.Sprintf("%f", errorRate), - "DEBUG=false") + "GOBUILDCACHE_BACKEND_TYPE=disk", + "GOBUILDCACHE_CACHE_DIR="+cacheDir, + "GOBUILDCACHE_ERROR_RATE="+fmt.Sprintf("%f", errorRate), + "GOBUILDCACHE_DEBUG=false") var testOutput bytes.Buffer testCmd.Stdout = &testOutput diff --git a/integrationtests/integration_s3_test.go b/integrationtests/integration_s3_test.go index 844453c..0441419 100644 --- a/integrationtests/integration_s3_test.go +++ b/integrationtests/integration_s3_test.go @@ -88,10 +88,10 @@ func TestCacheIntegrationS3(t *testing.T) { // Set environment to use S3 backend when Go starts the cache program firstRunCmd.Env = append(baseEnv, "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=s3", - "DEBUG=true", - "S3_BUCKET="+s3Bucket, - "S3_PREFIX="+bucketPrefix+"/") + "GOBUILDCACHE_BACKEND_TYPE=s3", + "GOBUILDCACHE_DEBUG=true", + "GOBUILDCACHE_S3_BUCKET="+s3Bucket, + "GOBUILDCACHE_S3_PREFIX="+bucketPrefix+"/") var firstRunOutput bytes.Buffer firstRunCmd.Stdout = &firstRunOutput @@ -115,10 +115,10 @@ func TestCacheIntegrationS3(t *testing.T) { // Set environment to use S3 backend when Go starts the cache program secondRunCmd.Env = append(baseEnv, "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=s3", - "DEBUG=true", - "S3_BUCKET="+s3Bucket, - "S3_PREFIX="+bucketPrefix+"/") + "GOBUILDCACHE_BACKEND_TYPE=s3", + "GOBUILDCACHE_DEBUG=true", + "GOBUILDCACHE_S3_BUCKET="+s3Bucket, + "GOBUILDCACHE_S3_PREFIX="+bucketPrefix+"/") var secondRunOutput bytes.Buffer secondRunCmd.Stdout = &secondRunOutput diff --git a/integrationtests/integration_test.go b/integrationtests/integration_test.go index 39b783d..1391f0c 100644 --- a/integrationtests/integration_test.go +++ b/integrationtests/integration_test.go @@ -63,10 +63,10 @@ func TestCacheIntegration(t *testing.T) { // Set environment to use disk backend when Go starts the cache program firstRunCmd.Env = append(os.Environ(), "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=disk", - "DEBUG=false", - "LOCK_TYPE=memory", - "CACHE_DIR="+cacheDir) + "GOBUILDCACHE_BACKEND_TYPE=disk", + "GOBUILDCACHE_DEBUG=false", + "GOBUILDCACHE_LOCK_TYPE=memory", + "GOBUILDCACHE_CACHE_DIR="+cacheDir) var firstRunOutput bytes.Buffer firstRunCmd.Stdout = &firstRunOutput @@ -90,10 +90,10 @@ func TestCacheIntegration(t *testing.T) { // Set environment to use disk backend when Go starts the cache program secondRunCmd.Env = append(os.Environ(), "GOCACHEPROG="+binaryPath, - "BACKEND_TYPE=disk", - "DEBUG=false", - "LOCK_TYPE=memory", - "CACHE_DIR="+cacheDir) + "GOBUILDCACHE_BACKEND_TYPE=disk", + "GOBUILDCACHE_DEBUG=false", + "GOBUILDCACHE_LOCK_TYPE=memory", + "GOBUILDCACHE_CACHE_DIR="+cacheDir) var secondRunOutput bytes.Buffer secondRunCmd.Stdout = &secondRunOutput diff --git a/main.go b/main.go index e5f3fc3..a66b237 100644 --- a/main.go +++ b/main.go @@ -58,19 +58,20 @@ func main() { func runServerCommand() { // Get defaults from environment variables. + // All variables support both GOBUILDCACHE_ and forms, with prefixed taking precedence. var ( serverFlags = flag.NewFlagSet("server", flag.ExitOnError) - debugDefault = getEnvBool("DEBUG", false) - printStatsDefault = getEnvBool("PRINT_STATS", true) - backendDefault = getEnv("BACKEND_TYPE", getEnv("BACKEND", "disk")) - lockTypeDefault = getEnv("LOCK_TYPE", "fslock") - lockDirDefault = getEnv("LOCK_DIR", filepath.Join(os.TempDir(), "gobuildcache", "locks")) - cacheDirDefault = getEnv("CACHE_DIR", filepath.Join(os.TempDir(), "gobuildcache", "cache")) - s3BucketDefault = getEnv("S3_BUCKET", "") - s3PrefixDefault = getEnv("S3_PREFIX", "gobuildcache/") - errorRateDefault = getEnvFloat("ERROR_RATE", 0.0) - compressionDefault = getEnvBool("COMPRESSION", true) - asyncBackendDefault = getEnvBool("ASYNC_BACKEND", true) + debugDefault = getEnvBoolWithPrefix("DEBUG", false) + printStatsDefault = getEnvBoolWithPrefix("PRINT_STATS", true) + backendDefault = getEnvWithPrefix("BACKEND_TYPE", getEnv("BACKEND", "disk")) + lockTypeDefault = getEnvWithPrefix("LOCK_TYPE", "fslock") + lockDirDefault = getEnvWithPrefix("LOCK_DIR", filepath.Join(os.TempDir(), "gobuildcache", "locks")) + cacheDirDefault = getEnvWithPrefix("CACHE_DIR", filepath.Join(os.TempDir(), "gobuildcache", "cache")) + s3BucketDefault = getEnvWithPrefix("S3_BUCKET", "") + s3PrefixDefault = getEnvWithPrefix("S3_PREFIX", "gobuildcache/") + errorRateDefault = getEnvFloatWithPrefix("ERROR_RATE", 0.0) + compressionDefault = getEnvBoolWithPrefix("COMPRESSION", true) + asyncBackendDefault = getEnvBoolWithPrefix("ASYNC_BACKEND", true) ) serverFlags.BoolVar(&debug, "debug", debugDefault, "Enable debug logging to stderr (env: DEBUG)") serverFlags.BoolVar(&printStats, "stats", printStatsDefault, "Print cache statistics on exit (env: PRINT_STATS)") @@ -90,6 +91,8 @@ func runServerCommand() { fmt.Fprintf(os.Stderr, "Flags (can also be set via environment variables):\n") serverFlags.PrintDefaults() fmt.Fprintf(os.Stderr, "\nEnvironment Variables:\n") + fmt.Fprintf(os.Stderr, " All variables support both GOBUILDCACHE_ and forms.\n") + fmt.Fprintf(os.Stderr, " The prefixed version takes precedence if both are set.\n\n") fmt.Fprintf(os.Stderr, " DEBUG Enable debug logging (true/false)\n") fmt.Fprintf(os.Stderr, " PRINT_STATS Print cache statistics on exit (true/false)\n") fmt.Fprintf(os.Stderr, " BACKEND_TYPE Backend type (disk, s3)\n") @@ -106,10 +109,12 @@ func runServerCommand() { fmt.Fprintf(os.Stderr, " %s -cache-dir=/var/cache/go\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Run with S3 backend using flags:\n") fmt.Fprintf(os.Stderr, " %s -backend=s3 -s3-bucket=my-cache-bucket\n\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " # Run with environment variables:\n") + fmt.Fprintf(os.Stderr, " # Run with environment variables (prefixed form):\n") + fmt.Fprintf(os.Stderr, " GOBUILDCACHE_BACKEND_TYPE=s3 GOBUILDCACHE_S3_BUCKET=my-cache-bucket %s\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " # Run with environment variables (unprefixed form, also supported):\n") fmt.Fprintf(os.Stderr, " BACKEND_TYPE=s3 S3_BUCKET=my-cache-bucket %s\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Mix environment variables and flags (flags override env):\n") - fmt.Fprintf(os.Stderr, " BACKEND_TYPE=s3 %s -s3-bucket=my-cache-bucket -debug\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " GOBUILDCACHE_BACKEND_TYPE=s3 %s -s3-bucket=my-cache-bucket -debug\n", os.Args[0]) } serverFlags.Parse(os.Args[1:]) @@ -118,13 +123,14 @@ func runServerCommand() { func runClearCommand() { // Get defaults from environment variables. + // All variables support both GOBUILDCACHE_ and forms, with prefixed taking precedence. var ( clearFlags = flag.NewFlagSet("clear", flag.ExitOnError) - debugDefault = getEnvBool("DEBUG", false) - backendDefault = getEnv("BACKEND_TYPE", getEnv("BACKEND", "disk")) - cacheDirDefault = getEnv("CACHE_DIR", filepath.Join(os.TempDir(), "gobuildcache", "cache")) - s3BucketDefault = getEnv("S3_BUCKET", "") - s3PrefixDefault = getEnv("S3_PREFIX", "") + debugDefault = getEnvBoolWithPrefix("DEBUG", false) + backendDefault = getEnvWithPrefix("BACKEND_TYPE", getEnv("BACKEND", "disk")) + cacheDirDefault = getEnvWithPrefix("CACHE_DIR", filepath.Join(os.TempDir(), "gobuildcache", "cache")) + s3BucketDefault = getEnvWithPrefix("S3_BUCKET", "") + s3PrefixDefault = getEnvWithPrefix("S3_PREFIX", "") ) clearFlags.BoolVar(&debug, "debug", debugDefault, "Enable debug logging to stderr (env: DEBUG)") clearFlags.StringVar(&backendType, "backend", backendDefault, "Backend type: disk (local only), s3 (env: BACKEND_TYPE)") @@ -138,6 +144,8 @@ func runClearCommand() { fmt.Fprintf(os.Stderr, "Flags (can also be set via environment variables):\n") clearFlags.PrintDefaults() fmt.Fprintf(os.Stderr, "\nEnvironment Variables:\n") + fmt.Fprintf(os.Stderr, " All variables support both GOBUILDCACHE_ and forms.\n") + fmt.Fprintf(os.Stderr, " The prefixed version takes precedence if both are set.\n\n") fmt.Fprintf(os.Stderr, " DEBUG Enable debug logging (true/false)\n") fmt.Fprintf(os.Stderr, " PRINT_STATS Print cache statistics on exit (true/false)\n") fmt.Fprintf(os.Stderr, " BACKEND_TYPE Backend type (disk, s3)\n") @@ -152,7 +160,7 @@ func runClearCommand() { fmt.Fprintf(os.Stderr, " # Clear S3 cache using flags:\n") fmt.Fprintf(os.Stderr, " %s clear -backend=s3 -s3-bucket=my-cache-bucket\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Clear using environment variables:\n") - fmt.Fprintf(os.Stderr, " BACKEND_TYPE=s3 S3_BUCKET=my-cache-bucket %s clear\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " GOBUILDCACHE_BACKEND_TYPE=s3 GOBUILDCACHE_S3_BUCKET=my-cache-bucket %s clear\n", os.Args[0]) } clearFlags.Parse(os.Args[2:]) @@ -160,11 +168,12 @@ func runClearCommand() { } func runClearLocalCommand() { - // Get defaults from environment variables + // Get defaults from environment variables. + // All variables support both GOBUILDCACHE_ and forms, with prefixed taking precedence. var ( clearLocalFlags = flag.NewFlagSet("clear-local", flag.ExitOnError) - debugDefault = getEnvBool("DEBUG", false) - cacheDirDefault = getEnv("CACHE_DIR", filepath.Join(os.TempDir(), "gobuildcache", "cache")) + debugDefault = getEnvBoolWithPrefix("DEBUG", false) + cacheDirDefault = getEnvWithPrefix("CACHE_DIR", filepath.Join(os.TempDir(), "gobuildcache", "cache")) ) clearLocalFlags.BoolVar(&debug, "debug", debugDefault, "Enable debug logging to stderr (env: DEBUG)") clearLocalFlags.StringVar(&cacheDir, "cache-dir", cacheDirDefault, "Local cache directory (env: CACHE_DIR)") @@ -175,6 +184,8 @@ func runClearLocalCommand() { fmt.Fprintf(os.Stderr, "Flags (can also be set via environment variables):\n") clearLocalFlags.PrintDefaults() fmt.Fprintf(os.Stderr, "\nEnvironment Variables:\n") + fmt.Fprintf(os.Stderr, " All variables support both GOBUILDCACHE_ and forms.\n") + fmt.Fprintf(os.Stderr, " The prefixed version takes precedence if both are set.\n\n") fmt.Fprintf(os.Stderr, " DEBUG Enable debug logging (true/false)\n") fmt.Fprintf(os.Stderr, " CACHE_DIR Local cache directory\n") fmt.Fprintf(os.Stderr, "\nNote: Command-line flags take precedence over environment variables.\n") @@ -184,7 +195,7 @@ func runClearLocalCommand() { fmt.Fprintf(os.Stderr, " # Clear local cache using custom directory:\n") fmt.Fprintf(os.Stderr, " %s clear-local -cache-dir=/var/cache/go\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Clear using environment variables:\n") - fmt.Fprintf(os.Stderr, " CACHE_DIR=/var/cache/go %s clear-local\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " GOBUILDCACHE_CACHE_DIR=/var/cache/go %s clear-local\n", os.Args[0]) } clearLocalFlags.Parse(os.Args[2:]) @@ -200,12 +211,13 @@ func runClearLocalCommand() { func runClearRemoteCommand() { // Get defaults from environment variables. + // All variables support both GOBUILDCACHE_ and forms, with prefixed taking precedence. var ( clearRemoteFlags = flag.NewFlagSet("clear-remote", flag.ExitOnError) - debugDefault = getEnvBool("DEBUG", false) - backendDefault = getEnv("BACKEND_TYPE", getEnv("BACKEND", "disk")) - s3BucketDefault = getEnv("S3_BUCKET", "") - s3PrefixDefault = getEnv("S3_PREFIX", "") + debugDefault = getEnvBoolWithPrefix("DEBUG", false) + backendDefault = getEnvWithPrefix("BACKEND_TYPE", getEnv("BACKEND", "disk")) + s3BucketDefault = getEnvWithPrefix("S3_BUCKET", "") + s3PrefixDefault = getEnvWithPrefix("S3_PREFIX", "") ) clearRemoteFlags.BoolVar(&debug, "debug", debugDefault, "Enable debug logging to stderr (env: DEBUG)") clearRemoteFlags.StringVar(&backendType, "backend", backendDefault, "Backend type: disk, s3 (env: BACKEND_TYPE)") @@ -218,6 +230,8 @@ func runClearRemoteCommand() { fmt.Fprintf(os.Stderr, "Flags (can also be set via environment variables):\n") clearRemoteFlags.PrintDefaults() fmt.Fprintf(os.Stderr, "\nEnvironment Variables:\n") + fmt.Fprintf(os.Stderr, " All variables support both GOBUILDCACHE_ and forms.\n") + fmt.Fprintf(os.Stderr, " The prefixed version takes precedence if both are set.\n\n") fmt.Fprintf(os.Stderr, " DEBUG Enable debug logging (true/false)\n") fmt.Fprintf(os.Stderr, " BACKEND_TYPE Backend type (disk, s3)\n") fmt.Fprintf(os.Stderr, " S3_BUCKET S3 bucket name\n") @@ -229,7 +243,7 @@ func runClearRemoteCommand() { fmt.Fprintf(os.Stderr, " # Clear S3 cache with prefix:\n") fmt.Fprintf(os.Stderr, " %s clear-remote -backend=s3 -s3-bucket=my-cache-bucket -s3-prefix=myproject/\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, " # Clear using environment variables:\n") - fmt.Fprintf(os.Stderr, " BACKEND_TYPE=s3 S3_BUCKET=my-cache-bucket %s clear-remote\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " GOBUILDCACHE_BACKEND_TYPE=s3 GOBUILDCACHE_S3_BUCKET=my-cache-bucket %s clear-remote\n", os.Args[0]) } clearRemoteFlags.Parse(os.Args[2:]) @@ -419,6 +433,18 @@ func getEnv(key, defaultValue string) string { return defaultValue } +// getEnvWithPrefix gets an environment variable, checking for GOBUILDCACHE_ prefix first. +// This allows users to use either GOBUILDCACHE_ or for configuration. +// The prefixed version takes precedence if set. +func getEnvWithPrefix(key, defaultValue string) string { + // Check for GOBUILDCACHE_ prefixed version first + if value := os.Getenv("GOBUILDCACHE_" + key); value != "" { + return value + } + // Fall back to unprefixed version + return getEnv(key, defaultValue) +} + // getEnvBool gets a boolean environment variable or returns a default value. // Accepts: true, false, 1, 0, yes, no (case insensitive). func getEnvBool(key string, defaultValue bool) bool { @@ -429,6 +455,23 @@ func getEnvBool(key string, defaultValue bool) bool { return value == "true" || value == "1" || value == "yes" } +// getEnvBoolWithPrefix gets a boolean environment variable, checking for GOBUILDCACHE_ prefix first. +// This allows users to use either GOBUILDCACHE_ or for configuration. +// The prefixed version takes precedence if set, but falls back to unprefixed if the prefixed value is invalid. +func getEnvBoolWithPrefix(key string, defaultValue bool) bool { + prefixedKey := "GOBUILDCACHE_" + key + if value := strings.ToLower(os.Getenv(prefixedKey)); value != "" { + if value == "true" || value == "1" || value == "yes" { + return true + } + if value == "false" || value == "0" || value == "no" { + return false + } + // Invalid prefixed value, fall through to unprefixed + } + return getEnvBool(key, defaultValue) +} + // getEnvFloat gets a float64 environment variable or returns a default value. func getEnvFloat(key string, defaultValue float64) float64 { value := os.Getenv(key) @@ -441,3 +484,18 @@ func getEnvFloat(key string, defaultValue float64) float64 { } return f } + +// getEnvFloatWithPrefix gets a float64 environment variable, checking for GOBUILDCACHE_ prefix first. +// This allows users to use either GOBUILDCACHE_ or for configuration. +// The prefixed version takes precedence if set, but falls back to unprefixed if the prefixed value is invalid. +func getEnvFloatWithPrefix(key string, defaultValue float64) float64 { + prefixedKey := "GOBUILDCACHE_" + key + if value := os.Getenv(prefixedKey); value != "" { + var f float64 + if _, err := fmt.Sscanf(value, "%f", &f); err == nil { + return f + } + // Invalid prefixed value, fall through to unprefixed + } + return getEnvFloat(key, defaultValue) +}