go-build/pkg/build/setup.go

302 lines
8.7 KiB
Go

package build
import (
"sort"
"dappco.re/go/build/internal/ax"
"dappco.re/go/core"
"dappco.re/go/core/io"
)
// SetupTool identifies a toolchain or installer surface required by the
// action-style setup phase.
type SetupTool string
const (
// SetupToolGo installs the Go toolchain.
SetupToolGo SetupTool = "go"
// SetupToolGarble installs garble for obfuscated Go and Wails builds.
SetupToolGarble SetupTool = "garble"
// SetupToolTask installs the Task CLI for Taskfile-driven builds.
SetupToolTask SetupTool = "task"
// SetupToolNode installs Node.js/Corepack for frontend-backed builds.
SetupToolNode SetupTool = "node"
// SetupToolWails installs the Wails CLI for Wails-backed builds.
SetupToolWails SetupTool = "wails"
// SetupToolPython installs Python for Conan and MkDocs flows.
SetupToolPython SetupTool = "python"
// SetupToolPHP installs PHP for Composer-backed builds.
SetupToolPHP SetupTool = "php"
// SetupToolComposer installs Composer for PHP builds.
SetupToolComposer SetupTool = "composer"
// SetupToolRust installs Rust/Cargo for Rust builds.
SetupToolRust SetupTool = "rust"
// SetupToolConan installs Conan for C++ builds.
SetupToolConan SetupTool = "conan"
// SetupToolMkDocs installs MkDocs for docs builds.
SetupToolMkDocs SetupTool = "mkdocs"
// SetupToolDeno installs Deno for manifest-backed or override-driven builds.
SetupToolDeno SetupTool = "deno"
)
// SetupStep describes one toolchain requirement in the setup plan.
type SetupStep struct {
Tool SetupTool `json:"tool"`
Reason string `json:"reason"`
}
// SetupPlan is the Go-side equivalent of the action setup orchestration.
// It is pure data: discovery + config in, setup requirements out.
type SetupPlan struct {
ProjectDir string
PrimaryStack string
PrimaryStackSuggestion string
FrontendDirs []string
LinuxPackages []string
Steps []SetupStep
}
// ComputeSetupPlan derives the action-style setup requirements from discovery
// and config. When discovery is nil the function performs a fresh DiscoverFull
// pass using the provided filesystem and directory.
func ComputeSetupPlan(fs io.Medium, dir string, cfg *BuildConfig, discovery *DiscoveryResult) (*SetupPlan, error) {
if fs == nil {
fs = io.Local
}
if discovery == nil {
var err error
discovery, err = DiscoverFull(fs, dir)
if err != nil {
return nil, err
}
}
configuredType := resolveConfiguredBuildType(cfg, discovery)
denoRequested := DenoRequested(configuredDenoBuild(cfg))
hasTaskfile := configuredType == string(ProjectTypeTaskfile) || discovery.HasTaskfile || containsProjectType(discovery.Types, ProjectTypeTaskfile)
hasWails := configuredType == string(ProjectTypeWails) || discovery.PrimaryStackSuggestion == "wails2"
hasCPP := configuredType == string(ProjectTypeCPP) || containsProjectType(discovery.Types, ProjectTypeCPP) || discovery.HasRootCMakeLists
hasDocs := configuredType == string(ProjectTypeDocs) || containsProjectType(discovery.Types, ProjectTypeDocs) || discovery.HasDocsConfig
hasPython := configuredType == string(ProjectTypePython) || containsProjectType(discovery.Types, ProjectTypePython)
hasPHP := configuredType == string(ProjectTypePHP) || containsProjectType(discovery.Types, ProjectTypePHP) || discovery.HasRootComposerJSON
hasRust := configuredType == string(ProjectTypeRust) || containsProjectType(discovery.Types, ProjectTypeRust) || discovery.HasRootCargoToml
hasNode := configuredType == string(ProjectTypeNode) || hasWails || discovery.HasPackageJSON
hasGo := configuredType == string(ProjectTypeGo) || hasWails || hasTaskfile || discovery.HasGoToolchain || containsProjectType(discovery.Types, ProjectTypeGo)
primaryStack := discovery.PrimaryStack
primaryStackSuggestion := discovery.PrimaryStackSuggestion
if configuredType != "" {
primaryStack = configuredType
primaryStackSuggestion = SuggestStack([]ProjectType{ProjectType(configuredType)})
}
linuxPackages := resolveSetupLinuxPackages(fs, configuredType, discovery, hasWails)
plan := &SetupPlan{
ProjectDir: dir,
PrimaryStack: primaryStack,
PrimaryStackSuggestion: primaryStackSuggestion,
FrontendDirs: ResolveFrontendSetupDirs(fs, dir, denoRequested),
LinuxPackages: linuxPackages,
}
if hasGo {
plan.addStep(SetupToolGo, "Go-backed build stack detected")
}
if cfg != nil && cfg.Build.Obfuscate {
plan.addStep(SetupToolGarble, "build.obfuscate is enabled")
}
if hasTaskfile {
plan.addStep(SetupToolTask, "Taskfile project detected")
}
if hasNode {
plan.addStep(SetupToolNode, "frontend package manifests detected")
}
if hasWails {
plan.addStep(SetupToolWails, "Wails stack detected")
}
if hasPython || hasCPP || hasDocs {
plan.addStep(SetupToolPython, pythonSetupReason(hasPython, hasCPP, hasDocs))
}
if hasPHP {
plan.addStep(SetupToolPHP, "composer.json detected")
plan.addStep(SetupToolComposer, "composer-backed build detected")
}
if hasRust {
plan.addStep(SetupToolRust, "Cargo.toml detected")
}
if hasCPP {
plan.addStep(SetupToolConan, "C++ stack detected")
}
if hasDocs {
plan.addStep(SetupToolMkDocs, "MkDocs config detected")
}
if discovery.HasDenoManifest || denoRequested {
plan.addStep(SetupToolDeno, "Deno manifest or override detected")
}
return plan, nil
}
func pythonSetupReason(hasPython, hasCPP, hasDocs bool) string {
switch {
case hasPython:
return "Python project detected"
case hasCPP && hasDocs:
return "docs and C++ setup relies on Python tooling"
case hasCPP:
return "C++ setup relies on Python tooling"
case hasDocs:
return "MkDocs setup relies on Python tooling"
default:
return "Python tooling required"
}
}
// ResolveFrontendSetupDirs returns frontend directories that participate in the
// action-style setup phase.
//
// dirs := build.ResolveFrontendSetupDirs(io.Local, ".", true)
// // ["./frontend"] when the project only has an empty frontend/ directory
// // ["./apps/web"] when a nested package.json is detected
func ResolveFrontendSetupDirs(fs io.Medium, dir string, allowEmptyFallback bool) []string {
if fs == nil {
fs = io.Local
}
var dirs []string
rootHasManifest := hasFrontendManifest(fs, dir)
frontendDir := ax.Join(dir, "frontend")
frontendHasManifest := fs.IsDir(frontendDir) && hasFrontendManifest(fs, frontendDir)
if rootHasManifest {
dirs = append(dirs, dir)
}
if frontendHasManifest {
dirs = append(dirs, frontendDir)
}
collectFrontendSetupDirs(fs, dir, 0, &dirs)
if len(dirs) == 0 && allowEmptyFallback {
if fs.IsDir(frontendDir) {
dirs = append(dirs, frontendDir)
} else {
dirs = append(dirs, dir)
}
}
return deduplicateAndSortPaths(dirs)
}
func collectFrontendSetupDirs(fs io.Medium, dir string, depth int, dirs *[]string) {
if depth >= 2 {
return
}
entries, err := fs.List(dir)
if err != nil {
return
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
name := entry.Name()
if shouldSkipSubtreeDir(name) || name == "frontend" {
continue
}
candidateDir := ax.Join(dir, name)
if hasFrontendManifest(fs, candidateDir) {
*dirs = append(*dirs, candidateDir)
}
collectFrontendSetupDirs(fs, candidateDir, depth+1, dirs)
}
}
func deduplicateAndSortPaths(paths []string) []string {
if len(paths) == 0 {
return nil
}
seen := make(map[string]struct{}, len(paths))
result := make([]string, 0, len(paths))
for _, path := range paths {
path = ax.Clean(path)
if path == "" {
continue
}
if _, ok := seen[path]; ok {
continue
}
seen[path] = struct{}{}
result = append(result, path)
}
sort.Strings(result)
return result
}
func configuredDenoBuild(cfg *BuildConfig) string {
if cfg == nil {
return ""
}
return core.Trim(cfg.Build.DenoBuild)
}
func resolveConfiguredBuildType(cfg *BuildConfig, discovery *DiscoveryResult) string {
if cfg != nil {
if value := core.Lower(core.Trim(cfg.Build.Type)); value != "" {
return value
}
}
if discovery != nil {
return core.Lower(core.Trim(discovery.ConfiguredType))
}
return ""
}
func resolveSetupLinuxPackages(fs io.Medium, configuredType string, discovery *DiscoveryResult, hasWails bool) []string {
if discovery == nil {
return nil
}
packages := deduplicateStrings(append([]string{}, discovery.LinuxPackages...))
if len(packages) > 0 {
return packages
}
if !hasWails && configuredType != string(ProjectTypeWails) {
return nil
}
distro := core.Trim(discovery.Distro)
if distro == "" {
distro = detectDistroVersion(fs)
}
return ResolveLinuxPackages([]ProjectType{ProjectTypeWails}, distro)
}
func (p *SetupPlan) addStep(tool SetupTool, reason string) {
if p == nil {
return
}
for _, step := range p.Steps {
if step.Tool == tool {
return
}
}
p.Steps = append(p.Steps, SetupStep{
Tool: tool,
Reason: reason,
})
}