diff --git a/docs/md/melange.md b/docs/md/melange.md index 9fe119ae8..51abc1a9d 100644 --- a/docs/md/melange.md +++ b/docs/md/melange.md @@ -14,8 +14,9 @@ toc: true ### Options ``` - -h, --help help for melange - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + -h, --help help for melange + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_build.md b/docs/md/melange_build.md index 50c8d5786..2a281641e 100644 --- a/docs/md/melange_build.md +++ b/docs/md/melange_build.md @@ -35,13 +35,13 @@ melange build [flags] --cache-dir string directory used for cached inputs (default "./melange-cache/") --cache-source string directory or bucket used for preloading the cache --cleanup when enabled, the temp dir used for the guest will be cleaned up after completion (default true) - --cpu string default CPU resources to use for builds - --cpumodel string default memory resources to use for builds + --cpu string default CPU resources to use + --cpumodel string default CPU model to use --create-build-log creates a package.log file containing a list of packages that were built by the command - --debug enables debug logging of build pipelines - --debug-runner when enabled, the builder pod will persist after the build succeeds or fails + --debug enables debug logging of pipelines + --debug-runner when enabled, the runner pod will persist after the build succeeds or fails --dependency-log string log dependencies to a specified file - --disk string disk size to use for builds + --disk string default disk size to use --empty-workspace whether the build workspace should be empty --env-file strings files to use for preloaded environment variables --generate-index whether to generate APKINDEX.tar.gz (default true) @@ -55,7 +55,7 @@ melange build [flags] --license string license to use for the build config file itself (default "NOASSERTION") --lint-require strings linters that must pass (default [dev,infodir,libtool/la-files,setuidgid,tempdir,usrmerge,varempty,worldwrite]) --lint-warn strings linters that will generate warnings (default [binaryarch,cudaruntimelib,dll,duplicate,dylib,lddcheck,maninfo,nonlinux,object,opt,pkgconf,python/docs,python/multiple,python/test,sbom,srv,staticarchive,strip,unsupportedarch,usrlocal]) - --memory string default memory resources to use for builds + --memory string default memory resources to use --namespace string namespace to use in package URLs in SBOM (eg wolfi, alpine) (default "unknown") --out-dir string directory where packages will be output (default "./packages/") --override-host-triplet-libc-substitution-flavor string override the flavor of libc for ${{host.triplet.*}} substitutions (e.g. gnu,musl) -- default is gnu (default "gnu") @@ -69,7 +69,7 @@ melange build [flags] --signing-key string key to use for signing --source-dir string directory used for included sources --strip-origin-name whether origin names should be stripped (for bootstrap) - --timeout duration default timeout for builds + --timeout duration default timeout --trace string where to write trace output --vars-file string file to use for preloaded build configuration variables --workspace-dir string directory used for the workspace at /home/build @@ -78,7 +78,8 @@ melange build [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_bump.md b/docs/md/melange_bump.md index 1acfc5796..929ed2922 100644 --- a/docs/md/melange_bump.md +++ b/docs/md/melange_bump.md @@ -35,7 +35,8 @@ melange bump [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_compile.md b/docs/md/melange_compile.md index ffc1085d3..2971477f7 100644 --- a/docs/md/melange_compile.md +++ b/docs/md/melange_compile.md @@ -34,11 +34,13 @@ melange compile [flags] --build-option strings build options to enable --cache-dir string directory used for cached inputs (default "./melange-cache/") --cache-source string directory or bucket used for preloading the cache - --cpu string default CPU resources to use for builds + --cpu string default CPU resources to use + --cpumodel string default CPU model to use --create-build-log creates a package.log file containing a list of packages that were built by the command - --debug enables debug logging of build pipelines - --debug-runner when enabled, the builder pod will persist after the build succeeds or fails + --debug enables debug logging of pipelines + --debug-runner when enabled, the runner pod will persist after the build succeeds or fails --dependency-log string log dependencies to a specified file + --disk string default disk size to use --empty-workspace whether the build workspace should be empty --env-file strings files to use for preloaded environment variables --fail-on-lint-warning turns linter warnings into failures @@ -47,22 +49,24 @@ melange compile [flags] --git-commit string commit hash of the git repository containing the build config file (defaults to detecting HEAD) --git-repo-url string URL of the git repository containing the build config file (defaults to detecting from configured git remotes) -h, --help help for compile + --ignore-signatures ignore repository signature verification -i, --interactive when enabled, attaches stdin with a tty to the pod on failure -k, --keyring-append strings path to extra keys to include in the build environment keyring --license string license to use for the build config file itself (default "NOASSERTION") --log-policy strings logging policy to use (default [builtin:stderr]) - --memory string default memory resources to use for builds + --memory string default memory resources to use --namespace string namespace to use in package URLs in SBOM (eg wolfi, alpine) (default "unknown") --out-dir string directory where packages will be output (default "./packages/") --package-append strings extra packages to install for each of the build environments --pipeline-dir string directory used to extend defined built-in pipelines + --pipeline-dirs strings directories used to extend defined built-in pipelines -r, --repository-append strings path to extra repositories to include in the build environment - --rm clean up intermediate artifacts (e.g. container images) + --rm clean up intermediate artifacts (e.g. container images, temp dirs) --runner string which runner to use to enable running commands, default is based on your platform. Options are ["bubblewrap" "docker" "qemu"] --signing-key string key to use for signing --source-dir string directory used for included sources --strip-origin-name whether origin names should be stripped (for bootstrap) - --timeout duration default timeout for builds + --timeout duration default timeout --vars-file string file to use for preloaded build configuration variables --workspace-dir string directory used for the workspace at /home/build ``` @@ -70,7 +74,8 @@ melange compile [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_completion.md b/docs/md/melange_completion.md index cca26a3b1..b75b314aa 100644 --- a/docs/md/melange_completion.md +++ b/docs/md/melange_completion.md @@ -57,7 +57,8 @@ melange completion [bash|zsh|fish|powershell] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_index.md b/docs/md/melange_index.md index 4a7eaeefc..1a74f1188 100644 --- a/docs/md/melange_index.md +++ b/docs/md/melange_index.md @@ -39,7 +39,8 @@ melange index [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_initramfs.md b/docs/md/melange_initramfs.md index bcc5cc566..900348912 100644 --- a/docs/md/melange_initramfs.md +++ b/docs/md/melange_initramfs.md @@ -57,7 +57,8 @@ melange initramfs [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_keygen.md b/docs/md/melange_keygen.md index 722a80f01..44992f6a1 100644 --- a/docs/md/melange_keygen.md +++ b/docs/md/melange_keygen.md @@ -35,7 +35,8 @@ melange keygen [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_license-check.md b/docs/md/melange_license-check.md index 0355b0c20..e2523339a 100644 --- a/docs/md/melange_license-check.md +++ b/docs/md/melange_license-check.md @@ -38,7 +38,8 @@ melange license-check file [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_lint.md b/docs/md/melange_lint.md index 339403c00..285af4e20 100644 --- a/docs/md/melange_lint.md +++ b/docs/md/melange_lint.md @@ -38,7 +38,8 @@ melange lint [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_package-version.md b/docs/md/melange_package-version.md index 4415cd6c3..373274f9d 100644 --- a/docs/md/melange_package-version.md +++ b/docs/md/melange_package-version.md @@ -39,7 +39,8 @@ melange package-version [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_query.md b/docs/md/melange_query.md index c993215cd..c88bc8b9d 100644 --- a/docs/md/melange_query.md +++ b/docs/md/melange_query.md @@ -35,7 +35,8 @@ melange query [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_scan.md b/docs/md/melange_scan.md index 036c35050..9d09eb8bf 100644 --- a/docs/md/melange_scan.md +++ b/docs/md/melange_scan.md @@ -37,7 +37,8 @@ melange scan bash.yaml ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_sign-index.md b/docs/md/melange_sign-index.md index ca1075c32..d23f9a458 100644 --- a/docs/md/melange_sign-index.md +++ b/docs/md/melange_sign-index.md @@ -42,7 +42,8 @@ melange sign-index [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_sign.md b/docs/md/melange_sign.md index 5fb43b36e..a2f6b1110 100644 --- a/docs/md/melange_sign.md +++ b/docs/md/melange_sign.md @@ -39,7 +39,8 @@ melange sign [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_source.md b/docs/md/melange_source.md index 07caf1265..7a963f3be 100644 --- a/docs/md/melange_source.md +++ b/docs/md/melange_source.md @@ -26,7 +26,8 @@ Commands for managing source code from melange configurations. ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_source_get.md b/docs/md/melange_source_get.md index 8464fc2c5..191135cba 100644 --- a/docs/md/melange_source_get.md +++ b/docs/md/melange_source_get.md @@ -38,9 +38,10 @@ melange source get [config.yaml] [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") - -o, --output string output directory for extracted source (default "./source") - --source-dir string directory where patches and other sources are located (defaults to ./package-name/) + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + -o, --output string output directory for extracted source (default "./source") + --source-dir string directory where patches and other sources are located (defaults to ./package-name/) ``` ### SEE ALSO diff --git a/docs/md/melange_source_pop.md b/docs/md/melange_source_pop.md index 1cb04a687..8f5358859 100644 --- a/docs/md/melange_source_pop.md +++ b/docs/md/melange_source_pop.md @@ -42,9 +42,10 @@ melange source pop [config.yaml] [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") - -o, --output string output directory for extracted source (default "./source") - --source-dir string directory where patches and other sources are located (defaults to ./package-name/) + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + -o, --output string output directory for extracted source (default "./source") + --source-dir string directory where patches and other sources are located (defaults to ./package-name/) ``` ### SEE ALSO diff --git a/docs/md/melange_test.md b/docs/md/melange_test.md index 4fa3496f7..6d939dfd8 100644 --- a/docs/md/melange_test.md +++ b/docs/md/melange_test.md @@ -32,17 +32,18 @@ melange test [flags] --arch strings architectures to build for (e.g., x86_64,ppc64le,arm64) -- default is all, unless specified in config --cache-dir string directory used for cached inputs --cache-source string directory or bucket used for preloading the cache - --cpu string default CPU resources to use for tests - --cpumodel string default CPU model to use for tests - --debug enables debug logging of test pipelines (sets -x for steps) - --debug-runner when enabled, the builder pod will persist after the build succeeds or fails - --disk string disk size to use for tests + --cpu string default CPU resources to use + --cpumodel string default CPU model to use + --debug enables debug logging of pipelines + --debug-runner when enabled, the runner pod will persist after the build succeeds or fails + --disk string default disk size to use --env-file strings files to use for preloaded environment variables -h, --help help for test --ignore-signatures ignore repository signature verification -i, --interactive when enabled, attaches stdin with a tty to the pod on failure -k, --keyring-append strings path to extra keys to include in the build environment keyring - --memory string default memory resources to use for tests + --memory string default memory resources to use + --package-append strings extra packages to install for each of the build environments --pipeline-dirs strings directories used to extend defined built-in pipelines -r, --repository-append strings path to extra repositories to include in the build environment --rm clean up intermediate artifacts (e.g. container images, temp dirs) (default true) @@ -50,14 +51,15 @@ melange test [flags] --source-dir string directory used for included sources --test-option strings build options to enable --test-package-append strings extra packages to install for each of the test environments - --timeout duration default timeout for tests + --timeout duration default timeout --workspace-dir string directory used for the workspace at /home/build ``` ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_update-cache.md b/docs/md/melange_update-cache.md index 013bb67a3..7fe9e3cfa 100644 --- a/docs/md/melange_update-cache.md +++ b/docs/md/melange_update-cache.md @@ -35,7 +35,8 @@ melange update-cache [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/docs/md/melange_version.md b/docs/md/melange_version.md index b6aea6203..2d84ee214 100644 --- a/docs/md/melange_version.md +++ b/docs/md/melange_version.md @@ -25,7 +25,8 @@ melange version [flags] ### Options inherited from parent commands ``` - --log-level string log level (e.g. debug, info, warn, error) (default "INFO") + --config-file string path to project config file (default: .melange.yaml in current directory) + --log-level string log level (e.g. debug, info, warn, error) (default "INFO") ``` ### SEE ALSO diff --git a/pkg/cli/build.go b/pkg/cli/build.go index 2912db94e..d9f492756 100644 --- a/pkg/cli/build.go +++ b/pkg/cli/build.go @@ -22,7 +22,6 @@ import ( "path/filepath" "runtime" "strings" - "time" apko_types "chainguard.dev/apko/pkg/build/types" "github.com/chainguard-dev/clog" @@ -44,16 +43,15 @@ const BuiltinPipelineDir = "/usr/share/melange/pipelines" // addBuildFlags registers all build command flags to the provided FlagSet using the BuildFlags struct func addBuildFlags(fs *pflag.FlagSet, flags *BuildFlags) { + // Set build-specific defaults before registering common flags. + flags.Remove = true + flags.CacheDir = "./melange-cache/" + addCommonFlags(fs, &flags.CommonFlags) + + // Build-specific flags. fs.StringVar(&flags.BuildDate, "build-date", "", "date used for the timestamps of the files inside the image") - fs.StringVar(&flags.WorkspaceDir, "workspace-dir", "", "directory used for the workspace at /home/build") fs.StringVar(&flags.PipelineDir, "pipeline-dir", "", "directory used to extend defined built-in pipelines") - fs.StringSliceVar(&flags.PipelineDirs, "pipeline-dirs", []string{}, "directories used to extend defined built-in pipelines") - fs.StringVar(&flags.SourceDir, "source-dir", "", "directory used for included sources") - fs.StringVar(&flags.CacheDir, "cache-dir", "./melange-cache/", "directory used for cached inputs") - fs.StringVar(&flags.CacheSource, "cache-source", "", "directory or bucket used for preloading the cache") - fs.StringVar(&flags.ApkCacheDir, "apk-cache-dir", "", "directory used for cached apk packages (default is system-defined cache directory)") fs.StringVar(&flags.SigningKey, "signing-key", "", "key to use for signing") - fs.StringSliceVar(&flags.EnvFiles, "env-file", []string{}, "files to use for preloaded environment variables") fs.StringVar(&flags.VarsFile, "vars-file", "", "file to use for preloaded build configuration variables") fs.BoolVar(&flags.GenerateIndex, "generate-index", true, "whether to generate APKINDEX.tar.gz") fs.BoolVar(&flags.EmptyWorkspace, "empty-workspace", false, "whether the build workspace should be empty") @@ -64,25 +62,11 @@ func addBuildFlags(fs *pflag.FlagSet, flags *BuildFlags) { fs.StringSliceVar(&flags.Archstrs, "arch", nil, "architectures to build for (e.g., x86_64,ppc64le,arm64) -- default is all, unless specified in config") fs.StringVar(&flags.Libc, "override-host-triplet-libc-substitution-flavor", "gnu", "override the flavor of libc for ${{host.triplet.*}} substitutions (e.g. gnu,musl) -- default is gnu") fs.StringSliceVar(&flags.BuildOption, "build-option", []string{}, "build options to enable") - fs.StringVar(&flags.Runner, "runner", "", fmt.Sprintf("which runner to use to enable running commands, default is based on your platform. Options are %q", build.GetAllRunners())) - fs.StringSliceVarP(&flags.ExtraKeys, "keyring-append", "k", []string{}, "path to extra keys to include in the build environment keyring") - fs.StringSliceVarP(&flags.ExtraRepos, "repository-append", "r", []string{}, "path to extra repositories to include in the build environment") - fs.StringSliceVar(&flags.ExtraPackages, "package-append", []string{}, "extra packages to install for each of the build environments") fs.BoolVar(&flags.CreateBuildLog, "create-build-log", false, "creates a package.log file containing a list of packages that were built by the command") fs.BoolVar(&flags.PersistLintResults, "persist-lint-results", false, "persist lint results to JSON files in packages/{arch}/ directory") - fs.BoolVar(&flags.Debug, "debug", false, "enables debug logging of build pipelines") - fs.BoolVar(&flags.DebugRunner, "debug-runner", false, "when enabled, the builder pod will persist after the build succeeds or fails") - fs.BoolVarP(&flags.Interactive, "interactive", "i", false, "when enabled, attaches stdin with a tty to the pod on failure") - fs.BoolVar(&flags.Remove, "rm", true, "clean up intermediate artifacts (e.g. container images, temp dirs)") - fs.StringVar(&flags.CPU, "cpu", "", "default CPU resources to use for builds") - fs.StringVar(&flags.CPUModel, "cpumodel", "", "default memory resources to use for builds") - fs.StringVar(&flags.Disk, "disk", "", "disk size to use for builds") - fs.StringVar(&flags.Memory, "memory", "", "default memory resources to use for builds") - fs.DurationVar(&flags.Timeout, "timeout", 0, "default timeout for builds") fs.StringVar(&flags.TraceFile, "trace", "", "where to write trace output") fs.StringSliceVar(&flags.LintRequire, "lint-require", linter.DefaultRequiredLinters(), "linters that must pass") fs.StringSliceVar(&flags.LintWarn, "lint-warn", linter.DefaultWarnLinters(), "linters that will generate warnings") - fs.BoolVar(&flags.IgnoreSignatures, "ignore-signatures", false, "ignore repository signature verification") fs.BoolVar(&flags.Cleanup, "cleanup", true, "when enabled, the temp dir used for the guest will be cleaned up after completion") fs.StringVar(&flags.ConfigFileGitCommit, "git-commit", "", "commit hash of the git repository containing the build config file (defaults to detecting HEAD)") fs.StringVar(&flags.ConfigFileGitRepoURL, "git-repo-url", "", "URL of the git repository containing the build config file (defaults to detecting from configured git remotes)") @@ -95,44 +79,26 @@ func addBuildFlags(fs *pflag.FlagSet, flags *BuildFlags) { // BuildFlags holds all parsed build command flags type BuildFlags struct { + CommonFlags + + // Build-specific fields: BuildDate string - WorkspaceDir string PipelineDir string - PipelineDirs []string - SourceDir string - CacheDir string - CacheSource string - ApkCacheDir string SigningKey string GenerateIndex bool EmptyWorkspace bool StripOriginName bool OutDir string Archstrs []string - ExtraKeys []string - ExtraRepos []string DependencyLog string - EnvFiles []string VarsFile string PurlNamespace string BuildOption []string CreateBuildLog bool PersistLintResults bool - Debug bool - DebugRunner bool - Interactive bool - Remove bool - Runner string - CPU string - CPUModel string - Memory string - Disk string - Timeout time.Duration - ExtraPackages []string Libc string LintRequire []string LintWarn []string - IgnoreSignatures bool Cleanup bool ConfigFileGitCommit string ConfigFileGitRepoURL string @@ -281,6 +247,10 @@ func buildCmd() *cobra.Command { ctx := cmd.Context() log := clog.FromContext(ctx) + if pc := ProjectConfigFromContext(ctx); pc != nil { + pc.ApplyToBuildFlags(flags, cmd.Flags()) + } + if flags.TraceFile != "" { w, err := os.Create(flags.TraceFile) // #nosec G304 - User-specified trace file output if err != nil { diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index 971fc2e37..386ef5584 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -30,6 +30,7 @@ import ( func New() *cobra.Command { var level slag.Level var gcplog bool + var configFile string cmd := &cobra.Command{ Use: "melange", DisableAutoGenTag: true, @@ -44,12 +45,27 @@ func New() *cobra.Command { slog.SetDefault(slog.New(charmlog.NewWithOptions(os.Stderr, charmlog.Options{ReportTimestamp: true, Level: charmlog.Level(level)}))) } + // Load project config file. + cfgPath := configFile + if cfgPath == "" { + cfgPath = FindProjectConfig() + } + if cfgPath != "" { + pc, err := LoadProjectConfig(cfgPath) + if err != nil { + return fmt.Errorf("loading project config: %w", err) + } + slog.Debug("loaded project config", "path", cfgPath) + cmd.SetContext(WithProjectConfig(cmd.Context(), pc)) + } + return nil }, } cmd.PersistentFlags().Var(&level, "log-level", "log level (e.g. debug, info, warn, error)") cmd.PersistentFlags().BoolVar(&gcplog, "gcplog", false, "use GCP logging") _ = cmd.PersistentFlags().MarkHidden("gcplog") + cmd.PersistentFlags().StringVar(&configFile, "config-file", "", "path to project config file (default: .melange.yaml in current directory)") cmd.AddCommand(buildCmd()) cmd.AddCommand(initramfsCmd()) diff --git a/pkg/cli/compile.go b/pkg/cli/compile.go index bf543b22f..6b97d451e 100644 --- a/pkg/cli/compile.go +++ b/pkg/cli/compile.go @@ -21,52 +21,74 @@ import ( "os" "path/filepath" "strings" - "time" apko_types "chainguard.dev/apko/pkg/build/types" "github.com/chainguard-dev/clog" "github.com/spf13/cobra" + "github.com/spf13/pflag" "go.opentelemetry.io/otel" "chainguard.dev/melange/pkg/build" ) +// CompileFlags holds all parsed compile command flags +type CompileFlags struct { + CommonFlags + + // Compile-specific fields: + Arch string // single string, required + BuildDate string + PipelineDir string + SigningKey string + VarsFile string + GenerateIndex bool + EmptyWorkspace bool + StripOriginName bool + OutDir string + DependencyLog string + PurlNamespace string + BuildOption []string + LogPolicy []string + CreateBuildLog bool + FailOnLintWarning bool + GenerateProvenance bool + ConfigFileGitCommit string + ConfigFileGitRepoURL string + ConfigFileLicense string +} + +// addCompileFlags registers all compile command flags to the provided FlagSet using the CompileFlags struct +func addCompileFlags(fs *pflag.FlagSet, flags *CompileFlags) { + // Set compile-specific defaults before registering common flags. + // Note: Remove (--rm) intentionally defaults to false for compile, + // unlike build/test which default to true. + flags.CacheDir = "./melange-cache/" + addCommonFlags(fs, &flags.CommonFlags) + + // Compile-specific flags. + fs.StringVar(&flags.Arch, "arch", "", "architectures to compile for") + fs.StringVar(&flags.BuildDate, "build-date", "", "date used for the timestamps of the files inside the image") + fs.StringVar(&flags.PipelineDir, "pipeline-dir", "", "directory used to extend defined built-in pipelines") + fs.StringVar(&flags.SigningKey, "signing-key", "", "key to use for signing") + fs.StringVar(&flags.VarsFile, "vars-file", "", "file to use for preloaded build configuration variables") + fs.BoolVar(&flags.GenerateIndex, "generate-index", true, "whether to generate APKINDEX.tar.gz") + fs.BoolVar(&flags.EmptyWorkspace, "empty-workspace", false, "whether the build workspace should be empty") + fs.BoolVar(&flags.StripOriginName, "strip-origin-name", false, "whether origin names should be stripped (for bootstrap)") + fs.StringVar(&flags.OutDir, "out-dir", "./packages/", "directory where packages will be output") + fs.StringVar(&flags.DependencyLog, "dependency-log", "", "log dependencies to a specified file") + fs.StringVar(&flags.PurlNamespace, "namespace", "unknown", "namespace to use in package URLs in SBOM (eg wolfi, alpine)") + fs.StringSliceVar(&flags.BuildOption, "build-option", []string{}, "build options to enable") + fs.StringSliceVar(&flags.LogPolicy, "log-policy", []string{"builtin:stderr"}, "logging policy to use") + fs.BoolVar(&flags.CreateBuildLog, "create-build-log", false, "creates a package.log file containing a list of packages that were built by the command") + fs.BoolVar(&flags.FailOnLintWarning, "fail-on-lint-warning", false, "turns linter warnings into failures") + fs.BoolVar(&flags.GenerateProvenance, "generate-provenance", false, "generate SLSA provenance for builds (included in a separate .attest.tar.gz file next to the APK)") + fs.StringVar(&flags.ConfigFileGitCommit, "git-commit", "", "commit hash of the git repository containing the build config file (defaults to detecting HEAD)") + fs.StringVar(&flags.ConfigFileGitRepoURL, "git-repo-url", "", "URL of the git repository containing the build config file (defaults to detecting from configured git remotes)") + fs.StringVar(&flags.ConfigFileLicense, "license", "NOASSERTION", "license to use for the build config file itself") +} + func compile() *cobra.Command { - var buildDate string - var workspaceDir string - var pipelineDir string - var sourceDir string - var cacheDir string - var cacheSource string - var apkCacheDir string - var signingKey string - var generateIndex bool - var emptyWorkspace bool - var stripOriginName bool - var outDir string - var archstr string - var extraKeys []string - var extraRepos []string - var dependencyLog string - var envFiles []string - var varsFile string - var purlNamespace string - var buildOption []string - var logPolicy []string - var createBuildLog bool - var debug bool - var debugRunner bool - var interactive bool - var remove bool - var runner string - var failOnLintWarning bool - var cpu, memory string - var timeout time.Duration - var extraPackages []string - var configFileGitCommit string - var configFileGitRepoURL string - var configFileLicense string - var generateProvenance bool + flags := &CompileFlags{} cmd := &cobra.Command{ Use: "compile", @@ -78,6 +100,10 @@ func compile() *cobra.Command { ctx := cmd.Context() log := clog.FromContext(ctx) + if pc := ProjectConfigFromContext(ctx); pc != nil { + pc.ApplyToCompileFlags(flags, cmd.Flags()) + } + var buildConfigFilePath string if len(args) > 0 { buildConfigFilePath = args[0] // e.g. "crane.yaml" @@ -87,71 +113,81 @@ func compile() *cobra.Command { // melange build definition. As a fallback, detect this from local git state. // Git auto-detection should be "best effort" and not fail the build if it // fails. - if configFileGitCommit == "" { + if flags.ConfigFileGitCommit == "" { log.Debugf("git commit for build config not provided, attempting to detect automatically") commit, err := detectGitHead(ctx, buildConfigFilePath) if err != nil { log.Warnf("unable to detect commit for build config file: %v", err) - configFileGitCommit = "unknown" + flags.ConfigFileGitCommit = "unknown" } else { - configFileGitCommit = commit + flags.ConfigFileGitCommit = commit } } - if configFileGitRepoURL == "" { + if flags.ConfigFileGitRepoURL == "" { log.Warnf("git repository URL for build config not provided") - configFileGitRepoURL = "https://unknown/unknown/unknown" + flags.ConfigFileGitRepoURL = "https://unknown/unknown/unknown" } - arch := apko_types.ParseArchitecture(archstr) + arch := apko_types.ParseArchitecture(flags.Arch) options := []build.Option{ build.WithArch(arch), - build.WithBuildDate(buildDate), - build.WithWorkspaceDir(workspaceDir), + build.WithBuildDate(flags.BuildDate), + build.WithWorkspaceDir(flags.WorkspaceDir), // Order matters, so add any specified pipelineDir before - // builtin pipelines. - build.WithPipelineDir(pipelineDir), - build.WithPipelineDir(BuiltinPipelineDir), - build.WithCacheDir(cacheDir), - build.WithCacheSource(cacheSource), - build.WithPackageCacheDir(apkCacheDir), - build.WithSigningKey(signingKey), - build.WithGenerateIndex(generateIndex), - build.WithEmptyWorkspace(emptyWorkspace), - build.WithOutDir(outDir), - build.WithExtraKeys(extraKeys), - build.WithExtraRepos(extraRepos), - build.WithExtraPackages(extraPackages), - build.WithDependencyLog(dependencyLog), - build.WithStripOriginName(stripOriginName), - build.WithEnvFiles(envFiles), - build.WithVarsFile(varsFile), - build.WithNamespace(purlNamespace), - build.WithEnabledBuildOptions(buildOption), - build.WithCreateBuildLog(createBuildLog), - build.WithDebug(debug), - build.WithDebugRunner(debugRunner), - build.WithInteractive(interactive), - build.WithRemove(remove), - build.WithCPU(cpu), - build.WithMemory(memory), - build.WithTimeout(timeout), - build.WithConfigFileRepositoryCommit(configFileGitCommit), - build.WithConfigFileRepositoryURL(configFileGitRepoURL), - build.WithConfigFileLicense(configFileLicense), - build.WithGenerateProvenance(generateProvenance), + // builtin pipelines. Support both --pipeline-dir (singular) + // and --pipeline-dirs (plural). + build.WithPipelineDir(flags.PipelineDir), + build.WithCacheDir(flags.CacheDir), + build.WithCacheSource(flags.CacheSource), + build.WithPackageCacheDir(flags.ApkCacheDir), + build.WithSigningKey(flags.SigningKey), + build.WithGenerateIndex(flags.GenerateIndex), + build.WithEmptyWorkspace(flags.EmptyWorkspace), + build.WithOutDir(flags.OutDir), + build.WithExtraKeys(flags.ExtraKeys), + build.WithExtraRepos(flags.ExtraRepos), + build.WithExtraPackages(flags.ExtraPackages), + build.WithDependencyLog(flags.DependencyLog), + build.WithStripOriginName(flags.StripOriginName), + build.WithEnvFiles(flags.EnvFiles), + build.WithVarsFile(flags.VarsFile), + build.WithNamespace(flags.PurlNamespace), + build.WithEnabledBuildOptions(flags.BuildOption), + build.WithCreateBuildLog(flags.CreateBuildLog), + build.WithDebug(flags.Debug), + build.WithDebugRunner(flags.DebugRunner), + build.WithInteractive(flags.Interactive), + build.WithRemove(flags.Remove), + build.WithCPU(flags.CPU), + build.WithCPUModel(flags.CPUModel), + build.WithDisk(flags.Disk), + build.WithMemory(flags.Memory), + build.WithTimeout(flags.Timeout), + build.WithIgnoreSignatures(flags.IgnoreSignatures), + build.WithConfigFileRepositoryCommit(flags.ConfigFileGitCommit), + build.WithConfigFileRepositoryURL(flags.ConfigFileGitRepoURL), + build.WithConfigFileLicense(flags.ConfigFileLicense), + build.WithGenerateProvenance(flags.GenerateProvenance), } if len(args) > 0 { options = append(options, build.WithConfig(args[0])) - if sourceDir == "" { - sourceDir = filepath.Dir(args[0]) + if flags.SourceDir == "" { + flags.SourceDir = filepath.Dir(args[0]) } } - if sourceDir != "" { - options = append(options, build.WithSourceDir(sourceDir)) + if flags.SourceDir != "" { + options = append(options, build.WithSourceDir(flags.SourceDir)) + } + + // Add multiple pipeline directories from --pipeline-dirs + for i := range flags.PipelineDirs { + options = append(options, build.WithPipelineDir(flags.PipelineDirs[i])) } + // Always append built-in pipeline directory as fallback + options = append(options, build.WithPipelineDir(BuiltinPipelineDir)) if auth, ok := os.LookupEnv("HTTP_AUTH"); !ok { // Fine, no auth. @@ -168,48 +204,12 @@ func compile() *cobra.Command { }, } - cmd.Flags().StringVar(&archstr, "arch", "", "architectures to compile for") + addCompileFlags(cmd.Flags(), flags) + if err := cmd.MarkFlagRequired("arch"); err != nil { panic(err) } - cmd.Flags().StringVar(&buildDate, "build-date", "", "date used for the timestamps of the files inside the image") - cmd.Flags().StringVar(&workspaceDir, "workspace-dir", "", "directory used for the workspace at /home/build") - cmd.Flags().StringVar(&pipelineDir, "pipeline-dir", "", "directory used to extend defined built-in pipelines") - cmd.Flags().StringVar(&sourceDir, "source-dir", "", "directory used for included sources") - cmd.Flags().StringVar(&cacheDir, "cache-dir", "./melange-cache/", "directory used for cached inputs") - cmd.Flags().StringVar(&cacheSource, "cache-source", "", "directory or bucket used for preloading the cache") - cmd.Flags().StringVar(&apkCacheDir, "apk-cache-dir", "", "directory used for cached apk packages (default is system-defined cache directory)") - cmd.Flags().StringVar(&signingKey, "signing-key", "", "key to use for signing") - cmd.Flags().StringSliceVar(&envFiles, "env-file", []string{}, "files to use for preloaded environment variables") - cmd.Flags().StringVar(&varsFile, "vars-file", "", "file to use for preloaded build configuration variables") - cmd.Flags().BoolVar(&generateIndex, "generate-index", true, "whether to generate APKINDEX.tar.gz") - cmd.Flags().BoolVar(&emptyWorkspace, "empty-workspace", false, "whether the build workspace should be empty") - cmd.Flags().BoolVar(&stripOriginName, "strip-origin-name", false, "whether origin names should be stripped (for bootstrap)") - cmd.Flags().StringVar(&outDir, "out-dir", "./packages/", "directory where packages will be output") - cmd.Flags().StringVar(&dependencyLog, "dependency-log", "", "log dependencies to a specified file") - cmd.Flags().StringVar(&purlNamespace, "namespace", "unknown", "namespace to use in package URLs in SBOM (eg wolfi, alpine)") - cmd.Flags().StringSliceVar(&buildOption, "build-option", []string{}, "build options to enable") - cmd.Flags().StringSliceVar(&logPolicy, "log-policy", []string{"builtin:stderr"}, "logging policy to use") - cmd.Flags().StringVar(&runner, "runner", "", fmt.Sprintf("which runner to use to enable running commands, default is based on your platform. Options are %q", build.GetAllRunners())) - cmd.Flags().StringSliceVarP(&extraKeys, "keyring-append", "k", []string{}, "path to extra keys to include in the build environment keyring") - cmd.Flags().StringSliceVarP(&extraRepos, "repository-append", "r", []string{}, "path to extra repositories to include in the build environment") - cmd.Flags().StringSliceVar(&extraPackages, "package-append", []string{}, "extra packages to install for each of the build environments") - cmd.Flags().BoolVar(&createBuildLog, "create-build-log", false, "creates a package.log file containing a list of packages that were built by the command") - cmd.Flags().BoolVar(&debug, "debug", false, "enables debug logging of build pipelines") - cmd.Flags().BoolVar(&debugRunner, "debug-runner", false, "when enabled, the builder pod will persist after the build succeeds or fails") - cmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "when enabled, attaches stdin with a tty to the pod on failure") - cmd.Flags().BoolVar(&remove, "rm", false, "clean up intermediate artifacts (e.g. container images)") - cmd.Flags().BoolVar(&failOnLintWarning, "fail-on-lint-warning", false, "turns linter warnings into failures") - cmd.Flags().StringVar(&cpu, "cpu", "", "default CPU resources to use for builds") - cmd.Flags().StringVar(&memory, "memory", "", "default memory resources to use for builds") - cmd.Flags().DurationVar(&timeout, "timeout", 0, "default timeout for builds") - cmd.Flags().BoolVar(&generateProvenance, "generate-provenance", false, "generate SLSA provenance for builds (included in a separate .attest.tar.gz file next to the APK)") - - cmd.Flags().StringVar(&configFileGitCommit, "git-commit", "", "commit hash of the git repository containing the build config file (defaults to detecting HEAD)") - cmd.Flags().StringVar(&configFileGitRepoURL, "git-repo-url", "", "URL of the git repository containing the build config file (defaults to detecting from configured git remotes)") - cmd.Flags().StringVar(&configFileLicense, "license", "NOASSERTION", "license to use for the build config file itself") - return cmd } diff --git a/pkg/cli/flags_common.go b/pkg/cli/flags_common.go new file mode 100644 index 000000000..19165c6e3 --- /dev/null +++ b/pkg/cli/flags_common.go @@ -0,0 +1,76 @@ +// Copyright 2025 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "fmt" + "time" + + "github.com/spf13/pflag" + + "chainguard.dev/melange/pkg/build" +) + +// CommonFlags holds flags shared across build, test, and compile commands. +type CommonFlags struct { + WorkspaceDir string + SourceDir string + CacheDir string + CacheSource string + ApkCacheDir string + Runner string + ExtraKeys []string // keyring-append + ExtraRepos []string // repository-append + EnvFiles []string // env-file + ExtraPackages []string // package-append + PipelineDirs []string + Debug bool + DebugRunner bool + Interactive bool + Remove bool // rm + IgnoreSignatures bool + CPU string + CPUModel string + Memory string + Disk string + Timeout time.Duration +} + +// addCommonFlags registers the shared flags onto fs, bound to the CommonFlags fields. +// The caller should pre-set any fields that need non-zero defaults (e.g. Remove, CacheDir) +// before calling this function — the current field values are used as the flag defaults. +func addCommonFlags(fs *pflag.FlagSet, flags *CommonFlags) { + fs.StringVar(&flags.WorkspaceDir, "workspace-dir", flags.WorkspaceDir, "directory used for the workspace at /home/build") + fs.StringSliceVar(&flags.PipelineDirs, "pipeline-dirs", flags.PipelineDirs, "directories used to extend defined built-in pipelines") + fs.StringVar(&flags.SourceDir, "source-dir", flags.SourceDir, "directory used for included sources") + fs.StringVar(&flags.CacheDir, "cache-dir", flags.CacheDir, "directory used for cached inputs") + fs.StringVar(&flags.CacheSource, "cache-source", flags.CacheSource, "directory or bucket used for preloading the cache") + fs.StringVar(&flags.ApkCacheDir, "apk-cache-dir", flags.ApkCacheDir, "directory used for cached apk packages (default is system-defined cache directory)") + fs.StringVar(&flags.Runner, "runner", flags.Runner, fmt.Sprintf("which runner to use to enable running commands, default is based on your platform. Options are %q", build.GetAllRunners())) + fs.StringSliceVarP(&flags.ExtraKeys, "keyring-append", "k", flags.ExtraKeys, "path to extra keys to include in the build environment keyring") + fs.StringSliceVarP(&flags.ExtraRepos, "repository-append", "r", flags.ExtraRepos, "path to extra repositories to include in the build environment") + fs.StringSliceVar(&flags.ExtraPackages, "package-append", flags.ExtraPackages, "extra packages to install for each of the build environments") + fs.StringSliceVar(&flags.EnvFiles, "env-file", flags.EnvFiles, "files to use for preloaded environment variables") + fs.BoolVar(&flags.Debug, "debug", flags.Debug, "enables debug logging of pipelines") + fs.BoolVar(&flags.DebugRunner, "debug-runner", flags.DebugRunner, "when enabled, the runner pod will persist after the build succeeds or fails") + fs.BoolVarP(&flags.Interactive, "interactive", "i", flags.Interactive, "when enabled, attaches stdin with a tty to the pod on failure") + fs.BoolVar(&flags.Remove, "rm", flags.Remove, "clean up intermediate artifacts (e.g. container images, temp dirs)") + fs.BoolVar(&flags.IgnoreSignatures, "ignore-signatures", flags.IgnoreSignatures, "ignore repository signature verification") + fs.StringVar(&flags.CPU, "cpu", flags.CPU, "default CPU resources to use") + fs.StringVar(&flags.CPUModel, "cpumodel", flags.CPUModel, "default CPU model to use") + fs.StringVar(&flags.Memory, "memory", flags.Memory, "default memory resources to use") + fs.StringVar(&flags.Disk, "disk", flags.Disk, "default disk size to use") + fs.DurationVar(&flags.Timeout, "timeout", flags.Timeout, "default timeout") +} diff --git a/pkg/cli/project_config.go b/pkg/cli/project_config.go new file mode 100644 index 000000000..dca5ed340 --- /dev/null +++ b/pkg/cli/project_config.go @@ -0,0 +1,131 @@ +// Copyright 2025 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "bytes" + "context" + "fmt" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +// DefaultProjectConfigFile is the default filename for project-level configuration. +const DefaultProjectConfigFile = ".melange.yaml" + +// ProjectConfig represents the top-level structure of a .melange.yaml project config file. +type ProjectConfig struct { + Global GlobalConfig `yaml:"global,omitempty"` + Build BuildSectionConfig `yaml:"build,omitempty"` + Test TestSectionConfig `yaml:"test,omitempty"` +} + +// GlobalConfig holds settings shared across build/test/compile subcommands. +type GlobalConfig struct { + Runner string `yaml:"runner,omitempty"` + SigningKey string `yaml:"signing-key,omitempty"` + KeyringAppend []string `yaml:"keyring-append,omitempty"` + RepositoryAppend []string `yaml:"repository-append,omitempty"` + Arch []string `yaml:"arch,omitempty"` + OutDir string `yaml:"out-dir,omitempty"` + CacheDir string `yaml:"cache-dir,omitempty"` + CacheSource string `yaml:"cache-source,omitempty"` + ApkCacheDir string `yaml:"apk-cache-dir,omitempty"` + Namespace string `yaml:"namespace,omitempty"` + PipelineDirs []string `yaml:"pipeline-dirs,omitempty"` + SourceDir string `yaml:"source-dir,omitempty"` + EnvFile []string `yaml:"env-file,omitempty"` + PackageAppend []string `yaml:"package-append,omitempty"` + Debug *bool `yaml:"debug,omitempty"` + DebugRunner *bool `yaml:"debug-runner,omitempty"` + Interactive *bool `yaml:"interactive,omitempty"` + Remove *bool `yaml:"rm,omitempty"` + IgnoreSignatures *bool `yaml:"ignore-signatures,omitempty"` + CPU string `yaml:"cpu,omitempty"` + CPUModel string `yaml:"cpumodel,omitempty"` + Memory string `yaml:"memory,omitempty"` + Disk string `yaml:"disk,omitempty"` + Timeout time.Duration `yaml:"timeout,omitempty"` +} + +// BuildSectionConfig holds build-specific settings that override GlobalConfig. +type BuildSectionConfig struct { + GlobalConfig `yaml:",inline"` + + GenerateIndex *bool `yaml:"generate-index,omitempty"` + EmptyWorkspace *bool `yaml:"empty-workspace,omitempty"` + StripOriginName *bool `yaml:"strip-origin-name,omitempty"` + DependencyLog string `yaml:"dependency-log,omitempty"` + VarsFile string `yaml:"vars-file,omitempty"` + BuildOption []string `yaml:"build-option,omitempty"` + CreateBuildLog *bool `yaml:"create-build-log,omitempty"` + PersistLintResults *bool `yaml:"persist-lint-results,omitempty"` + LintRequire []string `yaml:"lint-require,omitempty"` + LintWarn []string `yaml:"lint-warn,omitempty"` + Cleanup *bool `yaml:"cleanup,omitempty"` + GenerateProvenance *bool `yaml:"generate-provenance,omitempty"` + Libc string `yaml:"libc,omitempty"` +} + +// TestSectionConfig holds test-specific settings that override GlobalConfig. +type TestSectionConfig struct { + GlobalConfig `yaml:",inline"` + + TestOption []string `yaml:"test-option,omitempty"` + TestPackageAppend []string `yaml:"test-package-append,omitempty"` +} + +// LoadProjectConfig reads and parses a .melange.yaml project config file. +// Unknown fields in the YAML will cause an error, helping catch typos. +func LoadProjectConfig(path string) (*ProjectConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading project config %s: %w", path, err) + } + + var cfg ProjectConfig + decoder := yaml.NewDecoder(bytes.NewReader(data)) + decoder.KnownFields(true) + if err := decoder.Decode(&cfg); err != nil { + return nil, fmt.Errorf("parsing project config %s: %w", path, err) + } + + return &cfg, nil +} + +// FindProjectConfig looks for a .melange.yaml file in the current working directory. +// Returns the path if found, or an empty string if not found. +func FindProjectConfig() string { + if _, err := os.Stat(DefaultProjectConfigFile); err == nil { + return DefaultProjectConfigFile + } + return "" +} + +// projectConfigKey is the context key for storing the project config. +type projectConfigKey struct{} + +// WithProjectConfig returns a new context with the given project config stored in it. +func WithProjectConfig(ctx context.Context, cfg *ProjectConfig) context.Context { + return context.WithValue(ctx, projectConfigKey{}, cfg) +} + +// ProjectConfigFromContext retrieves the project config from the context, or nil if not set. +func ProjectConfigFromContext(ctx context.Context) *ProjectConfig { + cfg, _ := ctx.Value(projectConfigKey{}).(*ProjectConfig) + return cfg +} diff --git a/pkg/cli/project_config_apply.go b/pkg/cli/project_config_apply.go new file mode 100644 index 000000000..1563cda57 --- /dev/null +++ b/pkg/cli/project_config_apply.go @@ -0,0 +1,192 @@ +// Copyright 2025 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "time" + + "github.com/spf13/pflag" +) + +// applyCommonConfig applies project config values to CommonFlags fields, +// respecting CLI > sub-command config > global config precedence. +func applyCommonConfig(flags *CommonFlags, fs *pflag.FlagSet, sub *GlobalConfig, global *GlobalConfig) { + setStringIfUnchanged(fs, "runner", &flags.Runner, sub.Runner, global.Runner) + setStringSliceIfUnchanged(fs, "keyring-append", &flags.ExtraKeys, sub.KeyringAppend, global.KeyringAppend) + setStringSliceIfUnchanged(fs, "repository-append", &flags.ExtraRepos, sub.RepositoryAppend, global.RepositoryAppend) + setStringIfUnchanged(fs, "cache-dir", &flags.CacheDir, sub.CacheDir, global.CacheDir) + setStringIfUnchanged(fs, "cache-source", &flags.CacheSource, sub.CacheSource, global.CacheSource) + setStringIfUnchanged(fs, "apk-cache-dir", &flags.ApkCacheDir, sub.ApkCacheDir, global.ApkCacheDir) + setStringSliceIfUnchanged(fs, "pipeline-dirs", &flags.PipelineDirs, sub.PipelineDirs, global.PipelineDirs) + setStringIfUnchanged(fs, "source-dir", &flags.SourceDir, sub.SourceDir, global.SourceDir) + setStringSliceIfUnchanged(fs, "env-file", &flags.EnvFiles, sub.EnvFile, global.EnvFile) + setStringSliceIfUnchanged(fs, "package-append", &flags.ExtraPackages, sub.PackageAppend, global.PackageAppend) + setBoolPtrIfUnchanged(fs, "debug", &flags.Debug, sub.Debug, global.Debug) + setBoolPtrIfUnchanged(fs, "debug-runner", &flags.DebugRunner, sub.DebugRunner, global.DebugRunner) + setBoolPtrIfUnchanged(fs, "interactive", &flags.Interactive, sub.Interactive, global.Interactive) + setBoolPtrIfUnchanged(fs, "rm", &flags.Remove, sub.Remove, global.Remove) + setBoolPtrIfUnchanged(fs, "ignore-signatures", &flags.IgnoreSignatures, sub.IgnoreSignatures, global.IgnoreSignatures) + setStringIfUnchanged(fs, "cpu", &flags.CPU, sub.CPU, global.CPU) + setStringIfUnchanged(fs, "cpumodel", &flags.CPUModel, sub.CPUModel, global.CPUModel) + setStringIfUnchanged(fs, "memory", &flags.Memory, sub.Memory, global.Memory) + setStringIfUnchanged(fs, "disk", &flags.Disk, sub.Disk, global.Disk) + setDurationIfUnchanged(fs, "timeout", &flags.Timeout, sub.Timeout, global.Timeout) +} + +// ApplyToBuildFlags applies project config values to build flags, +// respecting CLI > config file > hardcoded defaults precedence. +// A flag is only overridden if it was not explicitly set on the CLI. +func (pc *ProjectConfig) ApplyToBuildFlags(flags *BuildFlags, fs *pflag.FlagSet) { + if pc == nil { + return + } + + g := &pc.Global + b := &pc.Build + + // Common flags shared across build/test/compile. + applyCommonConfig(&flags.CommonFlags, fs, &b.GlobalConfig, g) + + // Build has additional common-like fields with global fallback. + setStringIfUnchanged(fs, "signing-key", &flags.SigningKey, b.SigningKey, g.SigningKey) + setStringSliceIfUnchanged(fs, "arch", &flags.Archstrs, b.Arch, g.Arch) + setStringIfUnchanged(fs, "out-dir", &flags.OutDir, b.OutDir, g.OutDir) + setStringIfUnchanged(fs, "namespace", &flags.PurlNamespace, b.Namespace, g.Namespace) + + // Build-specific fields (no global fallback). + setBoolPtrIfUnchanged(fs, "generate-index", &flags.GenerateIndex, b.GenerateIndex, nil) + setBoolPtrIfUnchanged(fs, "empty-workspace", &flags.EmptyWorkspace, b.EmptyWorkspace, nil) + setBoolPtrIfUnchanged(fs, "strip-origin-name", &flags.StripOriginName, b.StripOriginName, nil) + setStringIfUnchanged(fs, "dependency-log", &flags.DependencyLog, b.DependencyLog, "") + setStringIfUnchanged(fs, "vars-file", &flags.VarsFile, b.VarsFile, "") + setStringSliceIfUnchanged(fs, "build-option", &flags.BuildOption, b.BuildOption, nil) + setBoolPtrIfUnchanged(fs, "create-build-log", &flags.CreateBuildLog, b.CreateBuildLog, nil) + setBoolPtrIfUnchanged(fs, "persist-lint-results", &flags.PersistLintResults, b.PersistLintResults, nil) + setStringSliceIfUnchanged(fs, "lint-require", &flags.LintRequire, b.LintRequire, nil) + setStringSliceIfUnchanged(fs, "lint-warn", &flags.LintWarn, b.LintWarn, nil) + setBoolPtrIfUnchanged(fs, "cleanup", &flags.Cleanup, b.Cleanup, nil) + setBoolPtrIfUnchanged(fs, "generate-provenance", &flags.GenerateProvenance, b.GenerateProvenance, nil) + setStringIfUnchanged(fs, "override-host-triplet-libc-substitution-flavor", &flags.Libc, b.Libc, "") +} + +// ApplyToTestFlags applies project config values to test flags, +// respecting CLI > config file > hardcoded defaults precedence. +func (pc *ProjectConfig) ApplyToTestFlags(flags *TestFlags, fs *pflag.FlagSet) { + if pc == nil { + return + } + + g := &pc.Global + t := &pc.Test + + // Common flags shared across build/test/compile. + applyCommonConfig(&flags.CommonFlags, fs, &t.GlobalConfig, g) + + // Test has additional common-like fields with global fallback. + setStringSliceIfUnchanged(fs, "arch", &flags.Archstrs, t.Arch, g.Arch) + + // Test-specific fields (no global fallback). + setStringSliceIfUnchanged(fs, "test-option", &flags.TestOption, t.TestOption, nil) + setStringSliceIfUnchanged(fs, "test-package-append", &flags.ExtraTestPackages, t.TestPackageAppend, nil) +} + +// ApplyToCompileFlags applies project config values to compile flags, +// respecting CLI > config file > hardcoded defaults precedence. +// Compile uses the build: section for its sub-command config (there is no +// separate compile: section), falling back to global:. +func (pc *ProjectConfig) ApplyToCompileFlags(flags *CompileFlags, fs *pflag.FlagSet) { + if pc == nil { + return + } + + g := &pc.Global + b := &pc.Build + + // Common flags shared across build/test/compile. + applyCommonConfig(&flags.CommonFlags, fs, &b.GlobalConfig, g) + + // Compile shares several fields with build. + setStringIfUnchanged(fs, "signing-key", &flags.SigningKey, b.SigningKey, g.SigningKey) + setStringIfUnchanged(fs, "out-dir", &flags.OutDir, b.OutDir, g.OutDir) + setStringIfUnchanged(fs, "namespace", &flags.PurlNamespace, b.Namespace, g.Namespace) + + // Compile-specific fields from build section (no global fallback). + setBoolPtrIfUnchanged(fs, "generate-index", &flags.GenerateIndex, b.GenerateIndex, nil) + setBoolPtrIfUnchanged(fs, "empty-workspace", &flags.EmptyWorkspace, b.EmptyWorkspace, nil) + setBoolPtrIfUnchanged(fs, "strip-origin-name", &flags.StripOriginName, b.StripOriginName, nil) + setStringIfUnchanged(fs, "dependency-log", &flags.DependencyLog, b.DependencyLog, "") + setStringIfUnchanged(fs, "vars-file", &flags.VarsFile, b.VarsFile, "") + setStringSliceIfUnchanged(fs, "build-option", &flags.BuildOption, b.BuildOption, nil) + setBoolPtrIfUnchanged(fs, "create-build-log", &flags.CreateBuildLog, b.CreateBuildLog, nil) + setBoolPtrIfUnchanged(fs, "generate-provenance", &flags.GenerateProvenance, b.GenerateProvenance, nil) +} + +// setStringIfUnchanged sets target to the first non-empty config value, +// but only if the named flag was not explicitly set on the command line. +// Note: an empty string in YAML (e.g. signing-key: "") is treated as "not set" +// and falls through to the next precedence level. +func setStringIfUnchanged(fs *pflag.FlagSet, name string, target *string, values ...string) { + if fs.Changed(name) { + return + } + for _, v := range values { + if v != "" { + *target = v + return + } + } +} + +// setBoolPtrIfUnchanged sets target to the first non-nil *bool config value, +// but only if the named flag was not explicitly set on the command line. +func setBoolPtrIfUnchanged(fs *pflag.FlagSet, name string, target *bool, values ...*bool) { + if fs.Changed(name) { + return + } + for _, v := range values { + if v != nil { + *target = *v + return + } + } +} + +// setStringSliceIfUnchanged sets target to the first non-nil, non-empty slice config value, +// but only if the named flag was not explicitly set on the command line. +func setStringSliceIfUnchanged(fs *pflag.FlagSet, name string, target *[]string, values ...[]string) { + if fs.Changed(name) { + return + } + for _, v := range values { + if len(v) > 0 { + *target = v + return + } + } +} + +// setDurationIfUnchanged sets target to the first non-zero duration config value, +// but only if the named flag was not explicitly set on the command line. +func setDurationIfUnchanged(fs *pflag.FlagSet, name string, target *time.Duration, values ...time.Duration) { + if fs.Changed(name) { + return + } + for _, v := range values { + if v != 0 { + *target = v + return + } + } +} diff --git a/pkg/cli/project_config_test.go b/pkg/cli/project_config_test.go new file mode 100644 index 000000000..bff3a501c --- /dev/null +++ b/pkg/cli/project_config_test.go @@ -0,0 +1,496 @@ +// Copyright 2025 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/spf13/pflag" +) + +func TestFlagDefaults_Remove(t *testing.T) { + // Build defaults --rm to true. + buildFlags := &BuildFlags{} + buildFS := pflag.NewFlagSet("build", pflag.ContinueOnError) + addBuildFlags(buildFS, buildFlags) + if !buildFlags.Remove { + t.Error("build --rm should default to true") + } + + // Test defaults --rm to true. + testFlags := &TestFlags{} + testFS := pflag.NewFlagSet("test", pflag.ContinueOnError) + addTestFlags(testFS, testFlags) + if !testFlags.Remove { + t.Error("test --rm should default to true") + } + + // Compile defaults --rm to false. + compileFlags := &CompileFlags{} + compileFS := pflag.NewFlagSet("compile", pflag.ContinueOnError) + addCompileFlags(compileFS, compileFlags) + if compileFlags.Remove { + t.Error("compile --rm should default to false") + } +} + +func TestFlagDefaults_CacheDir(t *testing.T) { + // Build defaults --cache-dir to "./melange-cache/". + buildFlags := &BuildFlags{} + buildFS := pflag.NewFlagSet("build", pflag.ContinueOnError) + addBuildFlags(buildFS, buildFlags) + if buildFlags.CacheDir != "./melange-cache/" { + t.Errorf("build --cache-dir = %q, want %q", buildFlags.CacheDir, "./melange-cache/") + } + + // Test defaults --cache-dir to "". + testFlags := &TestFlags{} + testFS := pflag.NewFlagSet("test", pflag.ContinueOnError) + addTestFlags(testFS, testFlags) + if testFlags.CacheDir != "" { + t.Errorf("test --cache-dir = %q, want %q", testFlags.CacheDir, "") + } + + // Compile defaults --cache-dir to "./melange-cache/". + compileFlags := &CompileFlags{} + compileFS := pflag.NewFlagSet("compile", pflag.ContinueOnError) + addCompileFlags(compileFS, compileFlags) + if compileFlags.CacheDir != "./melange-cache/" { + t.Errorf("compile --cache-dir = %q, want %q", compileFlags.CacheDir, "./melange-cache/") + } +} + +func TestLoadProjectConfig(t *testing.T) { + cfg, err := LoadProjectConfig("testdata/.melange.yaml") + if err != nil { + t.Fatalf("LoadProjectConfig: %v", err) + } + + // Global section + if cfg.Global.Runner != "bubblewrap" { + t.Errorf("Global.Runner = %q, want %q", cfg.Global.Runner, "bubblewrap") + } + if cfg.Global.SigningKey != "local-signing.rsa" { + t.Errorf("Global.SigningKey = %q, want %q", cfg.Global.SigningKey, "local-signing.rsa") + } + wantKeys := []string{"https://packages.wolfi.dev/os/wolfi-signing.rsa.pub"} + if diff := cmp.Diff(wantKeys, cfg.Global.KeyringAppend); diff != "" { + t.Errorf("Global.KeyringAppend mismatch (-want +got):\n%s", diff) + } + wantRepos := []string{"https://packages.wolfi.dev/os"} + if diff := cmp.Diff(wantRepos, cfg.Global.RepositoryAppend); diff != "" { + t.Errorf("Global.RepositoryAppend mismatch (-want +got):\n%s", diff) + } + wantArch := []string{"x86_64", "aarch64"} + if diff := cmp.Diff(wantArch, cfg.Global.Arch); diff != "" { + t.Errorf("Global.Arch mismatch (-want +got):\n%s", diff) + } + if cfg.Global.Namespace != "wolfi" { + t.Errorf("Global.Namespace = %q, want %q", cfg.Global.Namespace, "wolfi") + } + if cfg.Global.Debug == nil || !*cfg.Global.Debug { + t.Error("Global.Debug should be true") + } + + // Build section + if cfg.Build.Runner != "qemu" { + t.Errorf("Build.Runner = %q, want %q", cfg.Build.Runner, "qemu") + } + wantLint := []string{"dev", "opt"} + if diff := cmp.Diff(wantLint, cfg.Build.LintRequire); diff != "" { + t.Errorf("Build.LintRequire mismatch (-want +got):\n%s", diff) + } + if cfg.Build.GenerateIndex == nil || *cfg.Build.GenerateIndex != false { + t.Error("Build.GenerateIndex should be false") + } + + // Test section + wantTestPkgs := []string{"wolfi-base"} + if diff := cmp.Diff(wantTestPkgs, cfg.Test.TestPackageAppend); diff != "" { + t.Errorf("Test.TestPackageAppend mismatch (-want +got):\n%s", diff) + } +} + +func TestLoadProjectConfig_UnknownField(t *testing.T) { + _, err := LoadProjectConfig("testdata/.melange-invalid-field.yaml") + if err == nil { + t.Fatal("expected error for unknown field, got nil") + } + t.Logf("got expected error: %v", err) +} + +func TestApplyToBuildFlags_CLIOverridesConfig(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + }, + } + + flags := &BuildFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addBuildFlags(fs, flags) + + // Simulate CLI: --runner=docker + if err := fs.Parse([]string{"--runner=docker"}); err != nil { + t.Fatal(err) + } + + pc.ApplyToBuildFlags(flags, fs) + + if flags.Runner != "docker" { + t.Errorf("Runner = %q, want %q (CLI should override config)", flags.Runner, "docker") + } +} + +func TestApplyToBuildFlags_ConfigOverridesDefault(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + Namespace: "wolfi", + Debug: new(true), + }, + } + + flags := &BuildFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addBuildFlags(fs, flags) + + // No CLI flags set — parse empty args + if err := fs.Parse([]string{}); err != nil { + t.Fatal(err) + } + + pc.ApplyToBuildFlags(flags, fs) + + if flags.Runner != "bubblewrap" { + t.Errorf("Runner = %q, want %q", flags.Runner, "bubblewrap") + } + if flags.PurlNamespace != "wolfi" { + t.Errorf("PurlNamespace = %q, want %q", flags.PurlNamespace, "wolfi") + } + if !flags.Debug { + t.Error("Debug should be true from config") + } +} + +func TestApplyToBuildFlags_BuildOverridesGlobal(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + }, + Build: BuildSectionConfig{ + GlobalConfig: GlobalConfig{ + Runner: "qemu", + }, + }, + } + + flags := &BuildFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addBuildFlags(fs, flags) + if err := fs.Parse([]string{}); err != nil { + t.Fatal(err) + } + + pc.ApplyToBuildFlags(flags, fs) + + if flags.Runner != "qemu" { + t.Errorf("Runner = %q, want %q (build section should override global)", flags.Runner, "qemu") + } +} + +func TestApplyToBuildFlags_NilConfig(t *testing.T) { + flags := &BuildFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addBuildFlags(fs, flags) + + // Set a value after flag registration to simulate a default. + flags.Runner = "original" + + var pc *ProjectConfig + pc.ApplyToBuildFlags(flags, fs) + + if flags.Runner != "original" { + t.Errorf("Runner = %q, want %q (nil config should be no-op)", flags.Runner, "original") + } +} + +func TestApplyToCompileFlags_ConfigApplied(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + KeyringAppend: []string{ + "https://packages.wolfi.dev/os/wolfi-signing.rsa.pub", + }, + Namespace: "wolfi", + Debug: new(true), + }, + Build: BuildSectionConfig{ + GlobalConfig: GlobalConfig{ + Runner: "qemu", + }, + GenerateIndex: new(false), + }, + } + + flags := &CompileFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addCompileFlags(fs, flags) + if err := fs.Parse([]string{"--arch=x86_64"}); err != nil { + t.Fatal(err) + } + + pc.ApplyToCompileFlags(flags, fs) + + // Build section runner overrides global. + if flags.Runner != "qemu" { + t.Errorf("Runner = %q, want %q", flags.Runner, "qemu") + } + // Global keyring applied. + wantKeys := []string{"https://packages.wolfi.dev/os/wolfi-signing.rsa.pub"} + if diff := cmp.Diff(wantKeys, flags.ExtraKeys); diff != "" { + t.Errorf("ExtraKeys mismatch (-want +got):\n%s", diff) + } + // Global namespace applied. + if flags.PurlNamespace != "wolfi" { + t.Errorf("PurlNamespace = %q, want %q", flags.PurlNamespace, "wolfi") + } + // Global debug applied. + if !flags.Debug { + t.Error("Debug should be true from config") + } + // Build section generate-index applied. + if flags.GenerateIndex { + t.Error("GenerateIndex should be false from build config") + } +} + +func TestApplyToCompileFlags_ConfigOverridesDefault(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + Namespace: "wolfi", + Debug: new(true), + }, + } + + flags := &CompileFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addCompileFlags(fs, flags) + if err := fs.Parse([]string{"--arch=x86_64"}); err != nil { + t.Fatal(err) + } + + pc.ApplyToCompileFlags(flags, fs) + + if flags.Runner != "bubblewrap" { + t.Errorf("Runner = %q, want %q", flags.Runner, "bubblewrap") + } + if flags.PurlNamespace != "wolfi" { + t.Errorf("PurlNamespace = %q, want %q", flags.PurlNamespace, "wolfi") + } + if !flags.Debug { + t.Error("Debug should be true from config") + } +} + +func TestApplyToCompileFlags_BuildOverridesGlobal(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + }, + Build: BuildSectionConfig{ + GlobalConfig: GlobalConfig{ + Runner: "qemu", + }, + }, + } + + flags := &CompileFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addCompileFlags(fs, flags) + if err := fs.Parse([]string{"--arch=x86_64"}); err != nil { + t.Fatal(err) + } + + pc.ApplyToCompileFlags(flags, fs) + + if flags.Runner != "qemu" { + t.Errorf("Runner = %q, want %q (build section should override global)", flags.Runner, "qemu") + } +} + +func TestApplyToCompileFlags_NilConfig(t *testing.T) { + flags := &CompileFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addCompileFlags(fs, flags) + + flags.Runner = "original" + + var pc *ProjectConfig + pc.ApplyToCompileFlags(flags, fs) + + if flags.Runner != "original" { + t.Errorf("Runner = %q, want %q (nil config should be no-op)", flags.Runner, "original") + } +} + +func TestApplyToCompileFlags_CLIOverridesConfig(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + }, + } + + flags := &CompileFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addCompileFlags(fs, flags) + + // Simulate CLI: --runner=docker --arch=x86_64 + if err := fs.Parse([]string{"--runner=docker", "--arch=x86_64"}); err != nil { + t.Fatal(err) + } + + pc.ApplyToCompileFlags(flags, fs) + + if flags.Runner != "docker" { + t.Errorf("Runner = %q, want %q (CLI should override config)", flags.Runner, "docker") + } +} + +func TestApplyToTestFlags_ConfigApplied(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + KeyringAppend: []string{ + "https://packages.wolfi.dev/os/wolfi-signing.rsa.pub", + }, + }, + Test: TestSectionConfig{ + TestPackageAppend: []string{"wolfi-base"}, + }, + } + + flags := &TestFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addTestFlags(fs, flags) + if err := fs.Parse([]string{}); err != nil { + t.Fatal(err) + } + + pc.ApplyToTestFlags(flags, fs) + + if flags.Runner != "bubblewrap" { + t.Errorf("Runner = %q, want %q", flags.Runner, "bubblewrap") + } + wantKeys := []string{"https://packages.wolfi.dev/os/wolfi-signing.rsa.pub"} + if diff := cmp.Diff(wantKeys, flags.ExtraKeys); diff != "" { + t.Errorf("ExtraKeys mismatch (-want +got):\n%s", diff) + } + wantPkgs := []string{"wolfi-base"} + if diff := cmp.Diff(wantPkgs, flags.ExtraTestPackages); diff != "" { + t.Errorf("ExtraTestPackages mismatch (-want +got):\n%s", diff) + } +} + +func TestApplyToTestFlags_CLIOverridesConfig(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + }, + } + + flags := &TestFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addTestFlags(fs, flags) + + // Simulate CLI: --runner=docker + if err := fs.Parse([]string{"--runner=docker"}); err != nil { + t.Fatal(err) + } + + pc.ApplyToTestFlags(flags, fs) + + if flags.Runner != "docker" { + t.Errorf("Runner = %q, want %q (CLI should override config)", flags.Runner, "docker") + } +} + +func TestApplyToTestFlags_ConfigOverridesDefault(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + Debug: new(true), + }, + } + + flags := &TestFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addTestFlags(fs, flags) + if err := fs.Parse([]string{}); err != nil { + t.Fatal(err) + } + + pc.ApplyToTestFlags(flags, fs) + + if flags.Runner != "bubblewrap" { + t.Errorf("Runner = %q, want %q", flags.Runner, "bubblewrap") + } + if !flags.Debug { + t.Error("Debug should be true from config") + } +} + +func TestApplyToTestFlags_TestOverridesGlobal(t *testing.T) { + pc := &ProjectConfig{ + Global: GlobalConfig{ + Runner: "bubblewrap", + }, + Test: TestSectionConfig{ + GlobalConfig: GlobalConfig{ + Runner: "qemu", + }, + }, + } + + flags := &TestFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addTestFlags(fs, flags) + if err := fs.Parse([]string{}); err != nil { + t.Fatal(err) + } + + pc.ApplyToTestFlags(flags, fs) + + if flags.Runner != "qemu" { + t.Errorf("Runner = %q, want %q (test section should override global)", flags.Runner, "qemu") + } +} + +func TestApplyToTestFlags_NilConfig(t *testing.T) { + flags := &TestFlags{} + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addTestFlags(fs, flags) + + flags.Runner = "original" + + var pc *ProjectConfig + pc.ApplyToTestFlags(flags, fs) + + if flags.Runner != "original" { + t.Errorf("Runner = %q, want %q (nil config should be no-op)", flags.Runner, "original") + } +} diff --git a/pkg/cli/test.go b/pkg/cli/test.go index 868f30fc6..eaf017e3a 100644 --- a/pkg/cli/test.go +++ b/pkg/cli/test.go @@ -20,7 +20,6 @@ import ( "fmt" "os" "strings" - "time" apko_types "chainguard.dev/apko/pkg/build/types" "github.com/chainguard-dev/clog" @@ -34,56 +33,23 @@ import ( // addTestFlags registers all test command flags to the provided FlagSet using the TestFlags struct func addTestFlags(fs *pflag.FlagSet, flags *TestFlags) { - fs.StringVar(&flags.WorkspaceDir, "workspace-dir", "", "directory used for the workspace at /home/build") - fs.StringSliceVar(&flags.PipelineDirs, "pipeline-dirs", []string{}, "directories used to extend defined built-in pipelines") - fs.StringVar(&flags.SourceDir, "source-dir", "", "directory used for included sources") - fs.StringVar(&flags.CacheDir, "cache-dir", "", "directory used for cached inputs") - fs.StringVar(&flags.CacheSource, "cache-source", "", "directory or bucket used for preloading the cache") - fs.StringVar(&flags.ApkCacheDir, "apk-cache-dir", "", "directory used for cached apk packages (default is system-defined cache directory)") + // Set test-specific defaults before registering common flags. + flags.Remove = true + addCommonFlags(fs, &flags.CommonFlags) + + // Test-specific flags. fs.StringSliceVar(&flags.Archstrs, "arch", nil, "architectures to build for (e.g., x86_64,ppc64le,arm64) -- default is all, unless specified in config") fs.StringSliceVar(&flags.TestOption, "test-option", []string{}, "build options to enable") - fs.StringVar(&flags.Runner, "runner", "", fmt.Sprintf("which runner to use to enable running commands, default is based on your platform. Options are %q", build.GetAllRunners())) - fs.StringSliceVarP(&flags.ExtraKeys, "keyring-append", "k", []string{}, "path to extra keys to include in the build environment keyring") - fs.StringSliceVar(&flags.EnvFiles, "env-file", []string{}, "files to use for preloaded environment variables") - fs.BoolVar(&flags.Debug, "debug", false, "enables debug logging of test pipelines (sets -x for steps)") - fs.BoolVar(&flags.DebugRunner, "debug-runner", false, "when enabled, the builder pod will persist after the build succeeds or fails") - fs.BoolVarP(&flags.Interactive, "interactive", "i", false, "when enabled, attaches stdin with a tty to the pod on failure") - fs.StringSliceVarP(&flags.ExtraRepos, "repository-append", "r", []string{}, "path to extra repositories to include in the build environment") fs.StringSliceVar(&flags.ExtraTestPackages, "test-package-append", []string{}, "extra packages to install for each of the test environments") - fs.BoolVar(&flags.Remove, "rm", true, "clean up intermediate artifacts (e.g. container images, temp dirs)") - fs.BoolVar(&flags.IgnoreSignatures, "ignore-signatures", false, "ignore repository signature verification") - fs.StringVar(&flags.CPU, "cpu", "", "default CPU resources to use for tests") - fs.StringVar(&flags.CPUModel, "cpumodel", "", "default CPU model to use for tests") - fs.StringVar(&flags.Disk, "disk", "", "disk size to use for tests") - fs.StringVar(&flags.Memory, "memory", "", "default memory resources to use for tests") - fs.DurationVar(&flags.Timeout, "timeout", 0, "default timeout for tests") } // TestFlags holds all parsed test command flags type TestFlags struct { - WorkspaceDir string - SourceDir string - CacheDir string - CacheSource string - ApkCacheDir string + CommonFlags + Archstrs []string - PipelineDirs []string - ExtraKeys []string - ExtraRepos []string - EnvFiles []string TestOption []string - Debug bool - DebugRunner bool - Interactive bool - Runner string ExtraTestPackages []string - Remove bool - IgnoreSignatures bool - CPU string - CPUModel string - Memory string - Disk string - Timeout time.Duration } // ParseTestFlags parses test flags from the provided args and returns a TestFlags struct @@ -172,6 +138,11 @@ func test() *cobra.Command { Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() + + if pc := ProjectConfigFromContext(ctx); pc != nil { + pc.ApplyToTestFlags(flags, cmd.Flags()) + } + archs := apko_types.ParseArchitectures(flags.Archstrs) options, err := flags.TestOptions(ctx, args...) if err != nil { diff --git a/pkg/cli/testdata/.melange-invalid-field.yaml b/pkg/cli/testdata/.melange-invalid-field.yaml new file mode 100644 index 000000000..98b34f96d --- /dev/null +++ b/pkg/cli/testdata/.melange-invalid-field.yaml @@ -0,0 +1,3 @@ +global: + runner: bubblewrap + this-field-does-not-exist: true diff --git a/pkg/cli/testdata/.melange.yaml b/pkg/cli/testdata/.melange.yaml new file mode 100644 index 000000000..10bcb2f4a --- /dev/null +++ b/pkg/cli/testdata/.melange.yaml @@ -0,0 +1,25 @@ +global: + runner: bubblewrap + signing-key: local-signing.rsa + keyring-append: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + repository-append: + - https://packages.wolfi.dev/os + arch: + - x86_64 + - aarch64 + out-dir: ./packages/ + cache-dir: ./melange-cache/ + namespace: wolfi + debug: true + +build: + runner: qemu + lint-require: + - dev + - opt + generate-index: false + +test: + test-package-append: + - wolfi-base