Merge branch 'fix/io-migration-build' into new

# Conflicts:
#	pkg/build/checksum.go
#	pkg/build/config.go
#	pkg/build/discovery.go
This commit is contained in:
Snider 2026-02-08 21:28:17 +00:00
commit 650fd4d8c8
3 changed files with 74 additions and 64 deletions

View file

@ -6,25 +6,26 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io" "io"
"os"
"path/filepath" "path/filepath"
io_interface "github.com/host-uk/core/pkg/io"
"sort" "sort"
"strings" "strings"
coreio "github.com/host-uk/core/pkg/io"
) )
// Checksum computes SHA256 for an artifact and returns the artifact with the Checksum field filled. // Checksum computes SHA256 for an artifact and returns the artifact with the Checksum field filled.
func Checksum(fs io_interface.Medium, artifact Artifact) (Artifact, error) { func Checksum(artifact Artifact) (Artifact, error) {
if artifact.Path == "" { if artifact.Path == "" {
return Artifact{}, fmt.Errorf("build.Checksum: artifact path is empty") return Artifact{}, fmt.Errorf("build.Checksum: artifact path is empty")
} }
// Open the file // Open the file
file, err := fs.Open(artifact.Path) file, err := os.Open(artifact.Path)
if err != nil { if err != nil {
return Artifact{}, fmt.Errorf("build.Checksum: failed to open file: %w", err) return Artifact{}, fmt.Errorf("build.Checksum: failed to open file: %w", err)
} }
defer func() { _ = file.Close() }() defer file.Close()
// Compute SHA256 hash // Compute SHA256 hash
hasher := sha256.New() hasher := sha256.New()
@ -44,14 +45,14 @@ func Checksum(fs io_interface.Medium, artifact Artifact) (Artifact, error) {
// ChecksumAll computes checksums for all artifacts. // ChecksumAll computes checksums for all artifacts.
// Returns a slice of artifacts with their Checksum fields filled. // Returns a slice of artifacts with their Checksum fields filled.
func ChecksumAll(fs io_interface.Medium, artifacts []Artifact) ([]Artifact, error) { func ChecksumAll(artifacts []Artifact) ([]Artifact, error) {
if len(artifacts) == 0 { if len(artifacts) == 0 {
return nil, nil return nil, nil
} }
var checksummed []Artifact var checksummed []Artifact
for _, artifact := range artifacts { for _, artifact := range artifacts {
cs, err := Checksum(fs, artifact) cs, err := Checksum(artifact)
if err != nil { if err != nil {
return checksummed, fmt.Errorf("build.ChecksumAll: failed to checksum %s: %w", artifact.Path, err) return checksummed, fmt.Errorf("build.ChecksumAll: failed to checksum %s: %w", artifact.Path, err)
} }
@ -68,7 +69,7 @@ func ChecksumAll(fs io_interface.Medium, artifacts []Artifact) ([]Artifact, erro
// //
// The artifacts should have their Checksum fields filled (call ChecksumAll first). // The artifacts should have their Checksum fields filled (call ChecksumAll first).
// Filenames are relative to the output directory (just the basename). // Filenames are relative to the output directory (just the basename).
func WriteChecksumFile(fs io_interface.Medium, artifacts []Artifact, path string) error { func WriteChecksumFile(artifacts []Artifact, path string) error {
if len(artifacts) == 0 { if len(artifacts) == 0 {
return nil return nil
} }
@ -88,8 +89,14 @@ func WriteChecksumFile(fs io_interface.Medium, artifacts []Artifact, path string
content := strings.Join(lines, "\n") + "\n" content := strings.Join(lines, "\n") + "\n"
// Write the file using the medium (which handles directory creation in Write) // Ensure directory exists
if err := fs.Write(path, content); err != nil { dir := filepath.Dir(path)
if err := coreio.Local.EnsureDir(dir); err != nil {
return fmt.Errorf("build.WriteChecksumFile: failed to create directory: %w", err)
}
// Write the file
if err := coreio.Local.Write(path, content); err != nil {
return fmt.Errorf("build.WriteChecksumFile: failed to write file: %w", err) return fmt.Errorf("build.WriteChecksumFile: failed to write file: %w", err)
} }

View file

@ -4,11 +4,12 @@ package build
import ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"github.com/host-uk/core/pkg/build/signing" "github.com/host-uk/core/pkg/build/signing"
"github.com/host-uk/core/pkg/config"
"github.com/host-uk/core/pkg/io" "github.com/host-uk/core/pkg/io"
"gopkg.in/yaml.v3"
) )
// ConfigFileName is the name of the build configuration file. // ConfigFileName is the name of the build configuration file.
@ -21,75 +22,79 @@ const ConfigDir = ".core"
// This is distinct from Config which holds runtime build parameters. // This is distinct from Config which holds runtime build parameters.
type BuildConfig struct { type BuildConfig struct {
// Version is the config file format version. // Version is the config file format version.
Version int `yaml:"version" mapstructure:"version"` Version int `yaml:"version"`
// Project contains project metadata. // Project contains project metadata.
Project Project `yaml:"project" mapstructure:"project"` Project Project `yaml:"project"`
// Build contains build settings. // Build contains build settings.
Build Build `yaml:"build" mapstructure:"build"` Build Build `yaml:"build"`
// Targets defines the build targets. // Targets defines the build targets.
Targets []TargetConfig `yaml:"targets" mapstructure:"targets"` Targets []TargetConfig `yaml:"targets"`
// Sign contains code signing configuration. // Sign contains code signing configuration.
Sign signing.SignConfig `yaml:"sign,omitempty" mapstructure:"sign,omitempty"` Sign signing.SignConfig `yaml:"sign,omitempty"`
} }
// Project holds project metadata. // Project holds project metadata.
type Project struct { type Project struct {
// Name is the project name. // Name is the project name.
Name string `yaml:"name" mapstructure:"name"` Name string `yaml:"name"`
// Description is a brief description of the project. // Description is a brief description of the project.
Description string `yaml:"description" mapstructure:"description"` Description string `yaml:"description"`
// Main is the path to the main package (e.g., ./cmd/core). // Main is the path to the main package (e.g., ./cmd/core).
Main string `yaml:"main" mapstructure:"main"` Main string `yaml:"main"`
// Binary is the output binary name. // Binary is the output binary name.
Binary string `yaml:"binary" mapstructure:"binary"` Binary string `yaml:"binary"`
} }
// Build holds build-time settings. // Build holds build-time settings.
type Build struct { type Build struct {
// CGO enables CGO for the build. // CGO enables CGO for the build.
CGO bool `yaml:"cgo" mapstructure:"cgo"` CGO bool `yaml:"cgo"`
// Flags are additional build flags (e.g., ["-trimpath"]). // Flags are additional build flags (e.g., ["-trimpath"]).
Flags []string `yaml:"flags" mapstructure:"flags"` Flags []string `yaml:"flags"`
// LDFlags are linker flags (e.g., ["-s", "-w"]). // LDFlags are linker flags (e.g., ["-s", "-w"]).
LDFlags []string `yaml:"ldflags" mapstructure:"ldflags"` LDFlags []string `yaml:"ldflags"`
// Env are additional environment variables. // Env are additional environment variables.
Env []string `yaml:"env" mapstructure:"env"` Env []string `yaml:"env"`
} }
// TargetConfig defines a build target in the config file. // TargetConfig defines a build target in the config file.
// This is separate from Target to allow for additional config-specific fields. // This is separate from Target to allow for additional config-specific fields.
type TargetConfig struct { type TargetConfig struct {
// OS is the target operating system (e.g., "linux", "darwin", "windows"). // OS is the target operating system (e.g., "linux", "darwin", "windows").
OS string `yaml:"os" mapstructure:"os"` OS string `yaml:"os"`
// Arch is the target architecture (e.g., "amd64", "arm64"). // Arch is the target architecture (e.g., "amd64", "arm64").
Arch string `yaml:"arch" mapstructure:"arch"` Arch string `yaml:"arch"`
} }
// LoadConfig loads build configuration from the .core/build.yaml file in the given directory. // LoadConfig loads build configuration from the .core/build.yaml file in the given directory.
// If the config file does not exist, it returns DefaultConfig(). // If the config file does not exist, it returns DefaultConfig().
// Returns an error if the file exists but cannot be parsed. // Returns an error if the file exists but cannot be parsed.
func LoadConfig(fs io.Medium, dir string) (*BuildConfig, error) { func LoadConfig(dir string) (*BuildConfig, error) {
configPath := filepath.Join(dir, ConfigDir, ConfigFileName) configPath := filepath.Join(dir, ConfigDir, ConfigFileName)
if !fs.Exists(configPath) { // Convert to absolute path for io.Local
return DefaultConfig(), nil absPath, err := filepath.Abs(configPath)
}
// Use centralized config service
c, err := config.New(config.WithMedium(fs), config.WithPath(configPath))
if err != nil { if err != nil {
return nil, fmt.Errorf("build.LoadConfig: %w", err) return nil, fmt.Errorf("build.LoadConfig: failed to resolve path: %w", err)
} }
cfg := DefaultConfig() content, err := io.Local.Read(absPath)
if err := c.Get("", cfg); err != nil { if err != nil {
return nil, fmt.Errorf("build.LoadConfig: %w", err) if os.IsNotExist(err) {
return DefaultConfig(), nil
}
return nil, fmt.Errorf("build.LoadConfig: failed to read config file: %w", err)
} }
// Apply defaults for any missing fields (centralized Get might not fill everything) var cfg BuildConfig
applyDefaults(cfg) if err := yaml.Unmarshal([]byte(content), &cfg); err != nil {
return nil, fmt.Errorf("build.LoadConfig: failed to parse config file: %w", err)
}
return cfg, nil // Apply defaults for any missing fields
applyDefaults(&cfg)
return &cfg, nil
} }
// DefaultConfig returns sensible defaults for Go projects. // DefaultConfig returns sensible defaults for Go projects.
@ -110,6 +115,7 @@ func DefaultConfig() *BuildConfig {
Targets: []TargetConfig{ Targets: []TargetConfig{
{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "amd64"},
{OS: "linux", Arch: "arm64"}, {OS: "linux", Arch: "arm64"},
{OS: "darwin", Arch: "amd64"},
{OS: "darwin", Arch: "arm64"}, {OS: "darwin", Arch: "arm64"},
{OS: "windows", Arch: "amd64"}, {OS: "windows", Arch: "amd64"},
}, },
@ -155,8 +161,8 @@ func ConfigPath(dir string) string {
} }
// ConfigExists checks if a build config file exists in the given directory. // ConfigExists checks if a build config file exists in the given directory.
func ConfigExists(fs io.Medium, dir string) bool { func ConfigExists(dir string) bool {
return fs.IsFile(ConfigPath(dir)) return fileExists(ConfigPath(dir))
} }
// ToTargets converts TargetConfig slice to Target slice for use with builders. // ToTargets converts TargetConfig slice to Target slice for use with builders.

View file

@ -13,7 +13,6 @@ const (
markerWails = "wails.json" markerWails = "wails.json"
markerNodePackage = "package.json" markerNodePackage = "package.json"
markerComposer = "composer.json" markerComposer = "composer.json"
markerCMake = "CMakeLists.txt"
) )
// projectMarker maps a marker file to its project type. // projectMarker maps a marker file to its project type.
@ -29,18 +28,17 @@ var markers = []projectMarker{
{markerGoMod, ProjectTypeGo}, {markerGoMod, ProjectTypeGo},
{markerNodePackage, ProjectTypeNode}, {markerNodePackage, ProjectTypeNode},
{markerComposer, ProjectTypePHP}, {markerComposer, ProjectTypePHP},
{markerCMake, ProjectTypeCPP},
} }
// Discover detects project types in the given directory by checking for marker files. // Discover detects project types in the given directory by checking for marker files.
// Returns a slice of detected project types, ordered by priority (most specific first). // Returns a slice of detected project types, ordered by priority (most specific first).
// For example, a Wails project returns [wails, go] since it has both wails.json and go.mod. // For example, a Wails project returns [wails, go] since it has both wails.json and go.mod.
func Discover(fs io.Medium, dir string) ([]ProjectType, error) { func Discover(dir string) ([]ProjectType, error) {
var detected []ProjectType var detected []ProjectType
for _, m := range markers { for _, m := range markers {
path := filepath.Join(dir, m.file) path := filepath.Join(dir, m.file)
if fileExists(fs, path) { if fileExists(path) {
// Avoid duplicates (shouldn't happen with current markers, but defensive) // Avoid duplicates (shouldn't happen with current markers, but defensive)
if !slices.Contains(detected, m.projectType) { if !slices.Contains(detected, m.projectType) {
detected = append(detected, m.projectType) detected = append(detected, m.projectType)
@ -53,8 +51,8 @@ func Discover(fs io.Medium, dir string) ([]ProjectType, error) {
// PrimaryType returns the most specific project type detected in the directory. // PrimaryType returns the most specific project type detected in the directory.
// Returns empty string if no project type is detected. // Returns empty string if no project type is detected.
func PrimaryType(fs io.Medium, dir string) (ProjectType, error) { func PrimaryType(dir string) (ProjectType, error) {
types, err := Discover(fs, dir) types, err := Discover(dir)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -65,32 +63,31 @@ func PrimaryType(fs io.Medium, dir string) (ProjectType, error) {
} }
// IsGoProject checks if the directory contains a Go project (go.mod or wails.json). // IsGoProject checks if the directory contains a Go project (go.mod or wails.json).
func IsGoProject(fs io.Medium, dir string) bool { func IsGoProject(dir string) bool {
return fileExists(fs, filepath.Join(dir, markerGoMod)) || return fileExists(filepath.Join(dir, markerGoMod)) ||
fileExists(fs, filepath.Join(dir, markerWails)) fileExists(filepath.Join(dir, markerWails))
} }
// IsWailsProject checks if the directory contains a Wails project. // IsWailsProject checks if the directory contains a Wails project.
func IsWailsProject(fs io.Medium, dir string) bool { func IsWailsProject(dir string) bool {
return fileExists(fs, filepath.Join(dir, markerWails)) return fileExists(filepath.Join(dir, markerWails))
} }
// IsNodeProject checks if the directory contains a Node.js project. // IsNodeProject checks if the directory contains a Node.js project.
func IsNodeProject(fs io.Medium, dir string) bool { func IsNodeProject(dir string) bool {
return fileExists(fs, filepath.Join(dir, markerNodePackage)) return fileExists(filepath.Join(dir, markerNodePackage))
} }
// IsPHPProject checks if the directory contains a PHP project. // IsPHPProject checks if the directory contains a PHP project.
func IsPHPProject(fs io.Medium, dir string) bool { func IsPHPProject(dir string) bool {
return fileExists(fs, filepath.Join(dir, markerComposer)) return fileExists(filepath.Join(dir, markerComposer))
}
// IsCPPProject checks if the directory contains a C++ project.
func IsCPPProject(fs io.Medium, dir string) bool {
return fileExists(fs, filepath.Join(dir, markerCMake))
} }
// fileExists checks if a file exists and is not a directory. // fileExists checks if a file exists and is not a directory.
func fileExists(fs io.Medium, path string) bool { func fileExists(path string) bool {
return fs.IsFile(path) absPath, err := filepath.Abs(path)
if err != nil {
return false
}
return io.Local.IsFile(absPath)
} }