package cmd import ( "context" "errors" "flag" "fmt" "io/fs" "os" "path/filepath" "strconv" "time" "code.icb4dc0.de/buildr/buildr/internal/semver" "code.icb4dc0.de/buildr/buildr/modules/repo" "code.icb4dc0.de/buildr/buildr/modules/state" "code.icb4dc0.de/buildr/buildr/internal/config" "code.icb4dc0.de/buildr/buildr/internal/ignore" "code.icb4dc0.de/buildr/buildr/internal/profiling" "code.icb4dc0.de/buildr/buildr/internal/vault" "code.icb4dc0.de/buildr/buildr/modules/vcs" ) const defaultCacheTTL = 15 * time.Minute var ErrRepoRootNotFound = errors.New("failed to detect repo root") type AppConfig struct { Vault struct { FilePath string Passphrase string PassphraseFile string } VCSType vcs.Type BuildRDirectory string RepoRoot string State struct{ FilePath string } Profiling profiling.Config Cache struct{ TTL time.Duration } Execution struct { LogToStderr bool CleanDirectories bool } } func (c *AppConfig) CollectRepoDetails(vcsInfo any) (repoDetails repo.Repo, err error) { repoDetails.Root = c.RepoRoot if git, ok := vcsInfo.(*vcs.Git); ok && git.Tag != "" && semver.IsValid(git.Tag) { if repoDetails.Version, err = semver.ParseVersion(git.Tag); err != nil { return repoDetails, err } else { return repoDetails, nil } } return repoDetails, nil } func (c *AppConfig) ParseVCSInfo() (vcsDetails any, err error) { switch c.VCSType { case vcs.TypeGit: gitInfo, err := vcs.ParseGitInfo(c.RepoRoot) if err != nil { return nil, err } return gitInfo, nil default: return nil, fmt.Errorf("unsupported VCS type: %s", c.VCSType) } } func (c *AppConfig) RepoFS() fs.FS { return os.DirFS(c.RepoRoot) } func (c *AppConfig) BuildRFS() fs.FS { return os.DirFS(c.BuildRDirectory) } func (c *AppConfig) Ignorer() (*ignore.Ignorer, error) { return ignore.NewIgnorer(c.RepoRoot, c.Vault.FilePath) } func (c *AppConfig) InitPaths(from string) (err error) { if c.RepoRoot == "" { if c.RepoRoot, err = c.findRepoRoot(from); err != nil { return err } } if !filepath.IsAbs(c.BuildRDirectory) { c.BuildRDirectory = filepath.Join(c.RepoRoot, c.BuildRDirectory) } if stateFilePath := c.State.FilePath; stateFilePath != "" && !filepath.IsAbs(stateFilePath) { c.State.FilePath = filepath.Join(c.RepoRoot, stateFilePath) } return nil } //nolint:nilnil // if state is empty we don't care func (c *AppConfig) InitState(ctx context.Context) (*state.DB, error) { if stateFilePath := c.State.FilePath; stateFilePath != "" { return state.NewDB(ctx, c.State.FilePath) } return nil, nil } func (c *AppConfig) InitVault() (*vault.Vault, error) { var vaultOpts []vault.InitOption if pw := c.Vault.Passphrase; pw != "" { vaultOpts = append(vaultOpts, vault.WithPassphrase(pw)) } else if path := c.Vault.PassphraseFile; path != "" { vaultOpts = append(vaultOpts, vault.WithPassphraseFile(path)) } if filePath := c.Vault.FilePath; filePath != "" { if !filepath.IsAbs(c.Vault.FilePath) { filePath = filepath.Join(c.RepoRoot, filePath) } vaultOpts = append(vaultOpts, vault.WithLoadFromPath(filePath)) } return vault.NewVault(vaultOpts...) } func (c *AppConfig) Flags() *flag.FlagSet { flags := flag.NewFlagSet("buildr", flag.ExitOnError) c.VCSType = config.EnvOr("BUILDR_VCS_TYPE", vcs.ParseType, vcs.TypeGit) flags.StringVar( &c.BuildRDirectory, "buildr.directory", config.StringEnvOr("BUILDR_DIRECTORY", ".buildr"), "Directory where to look for BuildR files - if relative path will be relative to repository root (depending on the VCS directory)", ) flags.StringVar( &c.RepoRoot, "buildr.repo-root", config.StringEnvOr("BUILDR_REPO_ROOT", ""), "Repository root directory - normally auto-detected based on VCS directory", ) flags.StringVar( &c.Vault.FilePath, "vault.file", config.StringEnvOr("BUILDR_VAULT_FILE", filepath.Join(".buildr", "vault.json")), "Relative file path to vault file", ) flags.StringVar( &c.Vault.Passphrase, "vault.passphrase", config.StringEnvOr("BUILDR_VAULT_PASSPHRASE", ""), "Password for vault - has precedence over vault-passphrase-file", ) flags.StringVar( &c.Vault.PassphraseFile, "vault.passphrase-file", config.StringEnvOr("BUILDR_VAULT_PASSPHRASE_FILE", filepath.Join(".buildr", ".vaultpw")), "File containing the vault passphrase", ) flags.BoolVar( &c.Execution.LogToStderr, "execution.log-to-stderr", config.EnvOr("BUILDR_EXECUTION_LOG_TO_STDERR", strconv.ParseBool, false), "If output of commands should be piped to stdout", ) flags.BoolVar( &c.Execution.CleanDirectories, "execution.clean-directories", config.EnvOr("BUILDR_EXECUTION_CLEAN_DIRECTORIES", strconv.ParseBool, false), "If ephemeral directories (currently out and logs) should be cleaned", ) flags.StringVar( &c.State.FilePath, "state.file-path", config.StringEnvOr("BUILDR_STATE_FILE_PATH", filepath.Join(".buildr", "state.sqlite")), "Relative file path to state file", ) flags.DurationVar( &c.Cache.TTL, "cache.ttl", config.EnvOr("BUILDR_CACHE_TTL", time.ParseDuration, defaultCacheTTL), "TTL for cache entries", ) flags.Var( &c.VCSType, "vcs.type", "The type of VCS to use", ) c.Profiling.AddFlags(flags) return flags } func (c *AppConfig) findRepoRoot(dir string) (string, error) { if dir == "." || dir == "" || dir == "/" { return "", ErrRepoRootNotFound } info, err := os.Stat(filepath.Join(dir, c.VCSType.Directory())) if err != nil { return c.findRepoRoot(filepath.Dir(dir)) } if info.IsDir() { return dir, nil } return c.findRepoRoot(filepath.Dir(dir)) }