go-build/pkg/build/builders/taskfile.go
Virgil 112017d5f3 feat(build): export cgo state to Taskfile builds
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-01 21:13:33 +00:00

283 lines
7.5 KiB
Go

// Package builders provides build implementations for different project types.
package builders
import (
"context"
"path"
"runtime"
"dappco.re/go/core"
"dappco.re/go/core/build/internal/ax"
"dappco.re/go/core/build/pkg/build"
"dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
// TaskfileBuilder builds projects using Taskfile (https://taskfile.dev/).
// This is a generic builder that can handle any project type that has a Taskfile.
//
// b := builders.NewTaskfileBuilder()
type TaskfileBuilder struct{}
// NewTaskfileBuilder creates a new Taskfile builder.
//
// b := builders.NewTaskfileBuilder()
func NewTaskfileBuilder() *TaskfileBuilder {
return &TaskfileBuilder{}
}
// Name returns the builder's identifier.
//
// name := b.Name() // → "taskfile"
func (b *TaskfileBuilder) Name() string {
return "taskfile"
}
// Detect checks if a Taskfile exists in the directory.
//
// ok, err := b.Detect(io.Local, ".")
func (b *TaskfileBuilder) Detect(fs io.Medium, dir string) (bool, error) {
// Check for Taskfile.yml, Taskfile.yaml, or Taskfile
taskfiles := []string{
"Taskfile.yml",
"Taskfile.yaml",
"Taskfile",
"taskfile.yml",
"taskfile.yaml",
}
for _, tf := range taskfiles {
if fs.IsFile(ax.Join(dir, tf)) {
return true, nil
}
}
return false, nil
}
// Build runs the Taskfile build task for each target platform.
//
// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
func (b *TaskfileBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
taskCommand, err := b.resolveTaskCli()
if err != nil {
return nil, err
}
// Create output directory
outputDir := cfg.OutputDir
if outputDir == "" {
outputDir = ax.Join(cfg.ProjectDir, "dist")
}
if err := cfg.FS.EnsureDir(outputDir); err != nil {
return nil, coreerr.E("TaskfileBuilder.Build", "failed to create output directory", err)
}
var artifacts []build.Artifact
// If no targets are specified, build the host target so Taskfile builds
// still receive the standard GOOS/GOARCH surface.
if len(targets) == 0 {
targets = []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
}
// Run build task for each target
for _, target := range targets {
if err := b.runTask(ctx, cfg, taskCommand, outputDir, target); err != nil {
return nil, err
}
// Try to find artifacts for this target
found := b.findArtifactsForTarget(cfg.FS, outputDir, target)
artifacts = append(artifacts, found...)
}
return artifacts, nil
}
// runTask executes the Taskfile build task.
func (b *TaskfileBuilder) runTask(ctx context.Context, cfg *build.Config, taskCommand string, outputDir string, target build.Target) error {
// Build task command
args := []string{"build"}
env := append([]string{}, cfg.Env...)
platformDir := ax.Join(outputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
// Pass variables if targets are specified
if target.OS != "" {
value := core.Sprintf("GOOS=%s", target.OS)
args = append(args, value)
env = append(env, value)
}
if target.Arch != "" {
value := core.Sprintf("GOARCH=%s", target.Arch)
args = append(args, value)
env = append(env, value)
}
if target.OS != "" {
value := core.Sprintf("TARGET_OS=%s", target.OS)
args = append(args, value)
env = append(env, value)
}
if target.Arch != "" {
value := core.Sprintf("TARGET_ARCH=%s", target.Arch)
args = append(args, value)
env = append(env, value)
}
value := core.Sprintf("OUTPUT_DIR=%s", outputDir)
args = append(args, value)
env = append(env, value)
if platformDir != "" {
value := core.Sprintf("TARGET_DIR=%s", platformDir)
args = append(args, value)
env = append(env, value)
}
if cfg.Name != "" {
value := core.Sprintf("NAME=%s", cfg.Name)
args = append(args, value)
env = append(env, value)
}
if cfg.Version != "" {
value := core.Sprintf("VERSION=%s", cfg.Version)
args = append(args, value)
env = append(env, value)
}
value = "CGO_ENABLED=0"
if cfg.CGO {
value = "CGO_ENABLED=1"
}
args = append(args, value)
env = append(env, value)
if target.OS != "" && target.Arch != "" {
core.Print(nil, "Running task build for %s/%s", target.OS, target.Arch)
} else {
core.Print(nil, "Running task build")
}
if err := ax.ExecWithEnv(ctx, cfg.ProjectDir, env, taskCommand, args...); err != nil {
return coreerr.E("TaskfileBuilder.runTask", "task build failed", err)
}
return nil
}
// findArtifacts searches for built artifacts in the output directory.
func (b *TaskfileBuilder) findArtifacts(fs io.Medium, outputDir string) []build.Artifact {
var artifacts []build.Artifact
entries, err := fs.List(outputDir)
if err != nil {
return artifacts
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
// Skip common non-artifact files
name := entry.Name()
if core.HasPrefix(name, ".") || name == "CHECKSUMS.txt" {
continue
}
artifacts = append(artifacts, build.Artifact{
Path: ax.Join(outputDir, name),
OS: "",
Arch: "",
})
}
return artifacts
}
// findArtifactsForTarget searches for built artifacts for a specific target.
func (b *TaskfileBuilder) findArtifactsForTarget(fs io.Medium, outputDir string, target build.Target) []build.Artifact {
var artifacts []build.Artifact
// 1. Look for platform-specific subdirectory: output/os_arch/
platformSubdir := ax.Join(outputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
if fs.IsDir(platformSubdir) {
entries, _ := fs.List(platformSubdir)
for _, entry := range entries {
if entry.IsDir() {
// Handle .app bundles on macOS
if target.OS == "darwin" && core.HasSuffix(entry.Name(), ".app") {
artifacts = append(artifacts, build.Artifact{
Path: ax.Join(platformSubdir, entry.Name()),
OS: target.OS,
Arch: target.Arch,
})
}
continue
}
// Skip hidden files
if core.HasPrefix(entry.Name(), ".") {
continue
}
artifacts = append(artifacts, build.Artifact{
Path: ax.Join(platformSubdir, entry.Name()),
OS: target.OS,
Arch: target.Arch,
})
}
if len(artifacts) > 0 {
return artifacts
}
}
// 2. Look for files matching the target pattern in the root output dir
patterns := []string{
core.Sprintf("*-%s-%s*", target.OS, target.Arch),
core.Sprintf("*_%s_%s*", target.OS, target.Arch),
core.Sprintf("*-%s*", target.Arch),
}
for _, pattern := range patterns {
entries, _ := fs.List(outputDir)
for _, entry := range entries {
match := entry.Name()
// Simple glob matching
if b.matchPattern(match, pattern) {
fullPath := ax.Join(outputDir, match)
if fs.IsDir(fullPath) {
continue
}
artifacts = append(artifacts, build.Artifact{
Path: fullPath,
OS: target.OS,
Arch: target.Arch,
})
}
}
if len(artifacts) > 0 {
break // Found matches, stop looking
}
}
return artifacts
}
// matchPattern implements glob matching for Taskfile artifacts.
func (b *TaskfileBuilder) matchPattern(name, pattern string) bool {
matched, _ := path.Match(pattern, name)
return matched
}
// resolveTaskCli returns the executable path for the task CLI.
func (b *TaskfileBuilder) resolveTaskCli(paths ...string) (string, error) {
if len(paths) == 0 {
paths = []string{
"/usr/local/bin/task",
"/opt/homebrew/bin/task",
}
}
command, err := ax.ResolveCommand("task", paths...)
if err != nil {
return "", coreerr.E("TaskfileBuilder.resolveTaskCli", "task CLI not found. Install with: brew install go-task (macOS), go install github.com/go-task/task/v3/cmd/task@latest, or see https://taskfile.dev/installation/", err)
}
return command, nil
}