317 lines
8 KiB
Go
317 lines
8 KiB
Go
package builder
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"zsvo/pkg/recipe"
|
|
"zsvo/pkg/fetcher"
|
|
)
|
|
|
|
// Builder handles building packages from recipes
|
|
type Builder struct {
|
|
workDir string
|
|
}
|
|
|
|
// NewBuilder creates a new builder
|
|
func NewBuilder(workDir string) *Builder {
|
|
return &Builder{
|
|
workDir: workDir,
|
|
}
|
|
}
|
|
|
|
// Build builds a package from a recipe
|
|
func (b *Builder) Build(recipe *recipe.Recipe) error {
|
|
// Create working directories
|
|
sourceDir := recipe.GetSourceDir(b.workDir)
|
|
stagingDir := recipe.GetStagingDir(b.workDir)
|
|
packageDir := recipe.GetPackageDir(b.workDir)
|
|
|
|
if err := b.prepareDirectories(sourceDir, stagingDir, packageDir); err != nil {
|
|
return fmt.Errorf("failed to prepare directories: %w", err)
|
|
}
|
|
|
|
// Download and extract source
|
|
if err := b.downloadAndExtract(recipe); err != nil {
|
|
return fmt.Errorf("failed to download and extract source: %w", err)
|
|
}
|
|
|
|
// Apply patches
|
|
if err := b.applyPatches(recipe, sourceDir); err != nil {
|
|
return fmt.Errorf("failed to apply patches: %w", err)
|
|
}
|
|
|
|
// Build package
|
|
if err := b.executeBuild(recipe, sourceDir, stagingDir); err != nil {
|
|
return fmt.Errorf("failed to build package: %w", err)
|
|
}
|
|
|
|
// Package files
|
|
if err := b.packageFiles(recipe, sourceDir, stagingDir, packageDir); err != nil {
|
|
return fmt.Errorf("failed to package files: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// prepareDirectories creates necessary directories
|
|
func (b *Builder) prepareDirectories(sourceDir, stagingDir, packageDir string) error {
|
|
dirs := []string{
|
|
sourceDir,
|
|
stagingDir,
|
|
packageDir,
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// downloadAndExtract downloads and extracts source
|
|
func (b *Builder) downloadAndExtract(recipe *recipe.Recipe) error {
|
|
f := fetcher.NewFetcher(filepath.Join(b.workDir, "cache"))
|
|
return f.DownloadAndExtract(
|
|
recipe.Source.URL,
|
|
recipe.Source.Sha256,
|
|
recipe.GetSourceDir(b.workDir),
|
|
)
|
|
}
|
|
|
|
// applyPatches applies patches to source
|
|
func (b *Builder) applyPatches(recipe *recipe.Recipe, sourceDir string) error {
|
|
if len(recipe.Source.Patches) == 0 {
|
|
return nil
|
|
}
|
|
|
|
f := fetcher.NewFetcher(filepath.Join(b.workDir, "cache"))
|
|
return f.ApplyPatches(sourceDir, recipe.Source.Patches)
|
|
}
|
|
|
|
// executeBuild executes build commands
|
|
func (b *Builder) executeBuild(recipe *recipe.Recipe, sourceDir, stagingDir string) error {
|
|
// Find the source directory (usually the first subdirectory)
|
|
srcPath, err := b.findSourceDirectory(sourceDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find source directory: %w", err)
|
|
}
|
|
|
|
// Set up environment
|
|
env := b.buildEnvironment(recipe, stagingDir)
|
|
|
|
// Execute build commands
|
|
for i, cmd := range recipe.Build.Commands {
|
|
// Substitute variables in command
|
|
cmd = b.substituteVariables(cmd, recipe, sourceDir, stagingDir)
|
|
|
|
if err := b.executeCommand(srcPath, cmd, env); err != nil {
|
|
return fmt.Errorf("build command %d failed: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findSourceDirectory finds the actual source directory
|
|
func (b *Builder) findSourceDirectory(sourceDir string) (string, error) {
|
|
// Validate input path
|
|
if sourceDir == "" {
|
|
return "", fmt.Errorf("source directory cannot be empty")
|
|
}
|
|
|
|
// Clean and normalize path
|
|
sourceDir = filepath.Clean(sourceDir)
|
|
|
|
entries, err := os.ReadDir(sourceDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read source directory %s: %w", sourceDir, err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
return filepath.Join(sourceDir, entry.Name()), nil
|
|
}
|
|
}
|
|
|
|
return sourceDir, nil
|
|
}
|
|
|
|
// buildEnvironment builds the environment for build commands
|
|
func (b *Builder) buildEnvironment(recipe *recipe.Recipe, stagingDir string) []string {
|
|
env := os.Environ()
|
|
|
|
// Add standard build environment variables
|
|
env = append(env, fmt.Sprintf("DESTDIR=%s", stagingDir))
|
|
env = append(env, fmt.Sprintf("PREFIX=/usr"))
|
|
env = append(env, fmt.Sprintf("PKGDIR=%s", stagingDir))
|
|
|
|
// Add recipe-specific environment variables
|
|
for _, envVar := range recipe.Build.Env {
|
|
env = append(env, envVar)
|
|
}
|
|
|
|
// Add parallel build variable
|
|
env = append(env, fmt.Sprintf("MAKEFLAGS=-j%d", runtime.NumCPU()))
|
|
|
|
return env
|
|
}
|
|
|
|
// executeCommand executes a single command
|
|
func (b *Builder) executeCommand(workDir, command string, env []string) error {
|
|
// Parse command
|
|
args := strings.Fields(command)
|
|
if len(args) == 0 {
|
|
return nil
|
|
}
|
|
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.Dir = workDir
|
|
cmd.Env = env
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
// executeCommandWithOutput executes a command and returns output
|
|
func (b *Builder) executeCommandWithOutput(workDir, command string, env []string) (string, error) {
|
|
args := strings.Fields(command)
|
|
if len(args) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.Dir = workDir
|
|
cmd.Env = env
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
return string(output), err
|
|
}
|
|
|
|
// Clean removes build artifacts
|
|
func (b *Builder) Clean(recipe *recipe.Recipe) error {
|
|
dirs := []string{
|
|
recipe.GetSourceDir(b.workDir),
|
|
recipe.GetStagingDir(b.workDir),
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
if err := os.RemoveAll(dir); err != nil {
|
|
return fmt.Errorf("failed to clean directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBuildInfo returns information about the build
|
|
func (b *Builder) GetBuildInfo(recipe *recipe.Recipe) (*BuildInfo, error) {
|
|
sourceDir := recipe.GetSourceDir(b.workDir)
|
|
stagingDir := recipe.GetStagingDir(b.workDir)
|
|
|
|
info := &BuildInfo{
|
|
Recipe: recipe,
|
|
SourceDir: sourceDir,
|
|
StagingDir: stagingDir,
|
|
SourceExists: false,
|
|
StagingExists: false,
|
|
}
|
|
|
|
// Check if source directory exists
|
|
if _, err := os.Stat(sourceDir); err == nil {
|
|
info.SourceExists = true
|
|
}
|
|
|
|
// Check if staging directory exists
|
|
if _, err := os.Stat(stagingDir); err == nil {
|
|
info.StagingExists = true
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// BuildInfo contains information about a build
|
|
type BuildInfo struct {
|
|
Recipe *recipe.Recipe
|
|
SourceDir string
|
|
StagingDir string
|
|
SourceExists bool
|
|
StagingExists bool
|
|
}
|
|
|
|
// ListFilesInStaging lists all files in the staging directory
|
|
func (b *Builder) ListFilesInStaging(recipe *recipe.Recipe) ([]string, error) {
|
|
stagingDir := recipe.GetStagingDir(b.workDir)
|
|
var files []string
|
|
|
|
err := filepath.Walk(stagingDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
// Convert to relative path
|
|
relPath, err := filepath.Rel(stagingDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files = append(files, relPath)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return files, err
|
|
}
|
|
|
|
// GetWorkDir returns the working directory
|
|
func (b *Builder) GetWorkDir() string {
|
|
return b.workDir
|
|
}
|
|
|
|
// SetWorkDir sets the working directory
|
|
func (b *Builder) SetWorkDir(workDir string) {
|
|
b.workDir = workDir
|
|
}
|
|
|
|
// substituteVariables substitutes variables in command strings
|
|
func (b *Builder) substituteVariables(cmdStr string, recipe *recipe.Recipe, sourceDir, stagingDir string) string {
|
|
// Get number of CPU cores
|
|
jobs := runtime.NumCPU()
|
|
|
|
// Substitute variables
|
|
cmdStr = strings.ReplaceAll(cmdStr, "${jobs}", fmt.Sprintf("%d", jobs))
|
|
cmdStr = strings.ReplaceAll(cmdStr, "${pkgdir}", stagingDir)
|
|
cmdStr = strings.ReplaceAll(cmdStr, "${srcdir}", sourceDir)
|
|
|
|
return cmdStr
|
|
}
|
|
|
|
// packageFiles runs the package commands
|
|
func (b *Builder) packageFiles(recipe *recipe.Recipe, sourceDir, stagingDir, packageDir string) error {
|
|
// Find the source directory (usually the first subdirectory)
|
|
srcPath, err := b.findSourceDirectory(sourceDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find source directory: %w", err)
|
|
}
|
|
|
|
// Set up environment
|
|
env := b.buildEnvironment(recipe, stagingDir)
|
|
|
|
// Execute package commands
|
|
for i, cmd := range recipe.Package.Commands {
|
|
// Substitute variables in command
|
|
cmd = b.substituteVariables(cmd, recipe, sourceDir, stagingDir)
|
|
|
|
if err := b.executeCommand(srcPath, cmd, env); err != nil {
|
|
return fmt.Errorf("package command %d failed: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|