388 lines
11 KiB
Go
388 lines
11 KiB
Go
// 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"
|
|
|
|
"dappco.re/go/core"
|
|
"dappco.re/go/core/build/internal/ax"
|
|
"dappco.re/go/core/build/pkg/build/signing"
|
|
"dappco.re/go/core/io"
|
|
coreerr "dappco.re/go/core/log"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// 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(io.Local, ".")
|
|
type BuildConfig struct {
|
|
// Version is the config file format version.
|
|
Version int `yaml:"version"`
|
|
// Project contains project metadata.
|
|
Project Project `yaml:"project"`
|
|
// Build contains build settings.
|
|
Build Build `yaml:"build"`
|
|
// Targets defines the build targets.
|
|
Targets []TargetConfig `yaml:"targets"`
|
|
// Sign contains code signing configuration.
|
|
Sign signing.SignConfig `yaml:"sign,omitempty"`
|
|
}
|
|
|
|
// Project holds project metadata.
|
|
//
|
|
// cfg.Project.Binary = "core-build"
|
|
type Project struct {
|
|
// Name is the project name.
|
|
Name string `yaml:"name"`
|
|
// Description is a brief description of the project.
|
|
Description string `yaml:"description"`
|
|
// Main is the path to the main package (e.g., ./cmd/core).
|
|
Main string `yaml:"main"`
|
|
// Binary is the output binary name.
|
|
Binary string `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 `yaml:"type"`
|
|
// CGO enables CGO for the build.
|
|
CGO bool `yaml:"cgo"`
|
|
// Obfuscate uses garble instead of go build for binary obfuscation.
|
|
Obfuscate bool `yaml:"obfuscate"`
|
|
// NSIS enables Windows NSIS installer generation (Wails projects only).
|
|
NSIS bool `yaml:"nsis"`
|
|
// WebView2 sets the WebView2 delivery method: download|embed|browser|error.
|
|
WebView2 string `yaml:"webview2,omitempty"`
|
|
// Flags are additional build flags (e.g., ["-trimpath"]).
|
|
Flags []string `yaml:"flags"`
|
|
// LDFlags are linker flags (e.g., ["-s", "-w"]).
|
|
LDFlags []string `yaml:"ldflags"`
|
|
// BuildTags are Go build tags passed through to `go build`.
|
|
BuildTags []string `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 `yaml:"archive_format,omitempty"`
|
|
// Env are additional environment variables.
|
|
Env []string `yaml:"env"`
|
|
// Cache controls build cache setup.
|
|
Cache CacheConfig `yaml:"cache,omitempty"`
|
|
// Dockerfile is the path to the Dockerfile used by Docker builds.
|
|
Dockerfile string `yaml:"dockerfile,omitempty"`
|
|
// Registry is the container registry used for Docker image references.
|
|
Registry string `yaml:"registry,omitempty"`
|
|
// Image is the image name used for Docker builds.
|
|
Image string `yaml:"image,omitempty"`
|
|
// Tags are Docker image tags to apply.
|
|
Tags []string `yaml:"tags,omitempty"`
|
|
// BuildArgs are Docker build arguments.
|
|
BuildArgs map[string]string `yaml:"build_args,omitempty"`
|
|
// Push enables pushing Docker images after build.
|
|
Push bool `yaml:"push,omitempty"`
|
|
// Load loads a single-platform Docker image into the local daemon after build.
|
|
Load bool `yaml:"load,omitempty"`
|
|
// LinuxKitConfig is the path to the LinuxKit config file.
|
|
LinuxKitConfig string `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 `yaml:"formats,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 `yaml:"os"`
|
|
// Arch is the target architecture (e.g., "amd64", "arm64").
|
|
Arch string `yaml:"arch"`
|
|
}
|
|
|
|
// 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(io.Local, ".")
|
|
func LoadConfig(fs io.Medium, dir string) (*BuildConfig, error) {
|
|
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(io.Local, "/tmp/project/build.yaml")
|
|
func LoadConfigAtPath(fs io.Medium, configPath string) (*BuildConfig, error) {
|
|
content, err := fs.Read(configPath)
|
|
if err != nil {
|
|
if !fs.Exists(configPath) {
|
|
return DefaultConfig(), nil
|
|
}
|
|
return nil, coreerr.E("build.LoadConfigAtPath", "failed to read config file", err)
|
|
}
|
|
|
|
cfg := DefaultConfig()
|
|
data := []byte(content)
|
|
if err := yaml.Unmarshal(data, cfg); err != nil {
|
|
return nil, coreerr.E("build.LoadConfigAtPath", "failed to parse config file", err)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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: []TargetConfig{
|
|
{OS: "linux", Arch: "amd64"},
|
|
{OS: "linux", Arch: "arm64"},
|
|
{OS: "darwin", Arch: "arm64"},
|
|
{OS: "windows", Arch: "amd64"},
|
|
},
|
|
Sign: signing.DefaultSignConfig(),
|
|
}
|
|
}
|
|
|
|
// 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 = defaults.Targets
|
|
}
|
|
|
|
}
|
|
|
|
// 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.Targets = expandTargetConfigs(cfg.Targets)
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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(io.Local, ".") { ... }
|
|
func ConfigExists(fs io.Medium, dir string) bool {
|
|
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
|
|
}
|