feat(build): expand env vars in build config

This commit is contained in:
Virgil 2026-04-01 19:32:07 +00:00
parent 489e118779
commit 479612fdf7
2 changed files with 189 additions and 1 deletions

View file

@ -5,6 +5,7 @@ package build
import (
"iter"
"dappco.re/go/core"
"dappco.re/go/core/build/internal/ax"
"dappco.re/go/core/build/pkg/build/signing"
"dappco.re/go/core/io"
@ -144,6 +145,10 @@ func LoadConfigAtPath(fs io.Medium, configPath string) (*BuildConfig, 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()
return cfg, nil
}
@ -202,10 +207,117 @@ func applyDefaults(cfg *BuildConfig) {
cfg.Targets = defaults.Targets
}
// Expand environment variables in sign config
}
// 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.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 = expandEnv(cfg.Build.LinuxKitConfig)
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 = expandEnvSlice(cfg.Build.Formats)
cfg.Build.Cache.Directory = expandEnv(cfg.Build.Cache.Directory)
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.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
}
// 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"

View file

@ -84,6 +84,82 @@ targets:
assert.Equal(t, "arm64", cfg.Targets[1].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("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}
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, err := LoadConfig(fs, dir)
require.NoError(t, err)
require.NotNil(t, cfg)
assert.Equal(t, "demo-app", cfg.Project.Name)
assert.Equal(t, "./cmd/demo", cfg.Project.Main)
assert.Equal(t, "demo-bin", cfg.Project.Binary)
assert.Equal(t, "wails", cfg.Build.Type)
assert.Equal(t, "embed", cfg.Build.WebView2)
assert.Equal(t, "xz", cfg.Build.ArchiveFormat)
assert.Equal(t, []string{"-trimpath", "-X", "main.version=v1.2.3"}, cfg.Build.Flags)
assert.Equal(t, []string{"-s", "-w"}, cfg.Build.LDFlags)
assert.Equal(t, []string{"integration"}, cfg.Build.BuildTags)
assert.Equal(t, []string{"VERSION=v1.2.3"}, cfg.Build.Env)
assert.Equal(t, ".core/cache/demo-app", cfg.Build.Cache.Directory)
assert.Equal(t, []string{".core/cache/demo-app/go-build"}, cfg.Build.Cache.Paths)
assert.Equal(t, "Dockerfile.release", cfg.Build.Dockerfile)
assert.Equal(t, "owner/demo-app", cfg.Build.Image)
assert.Equal(t, []string{"latest", "v1.2.3"}, cfg.Build.Tags)
assert.Equal(t, map[string]string{"VERSION": "v1.2.3"}, cfg.Build.BuildArgs)
assert.Equal(t, "ABCD1234", cfg.Sign.GPG.Key)
})
t.Run("returns defaults when config file missing", func(t *testing.T) {
dir := t.TempDir()