Merge branch 'chore/io-migrate-build-8873543635510272463' into new

# Conflicts:
#	pkg/build/checksum.go
#	pkg/build/config.go
#	pkg/build/discovery.go
#	pkg/build/discovery_test.go
#	pkg/io/io.go
#	pkg/io/local/client.go
#	pkg/release/release.go
This commit is contained in:
Snider 2026-02-08 21:29:14 +00:00
commit fd4cbdee8f
5 changed files with 54 additions and 84 deletions

View file

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

View file

@ -69,16 +69,10 @@ type TargetConfig struct {
// LoadConfig loads build configuration from the .core/build.yaml file in the given directory.
// If the config file does not exist, it returns DefaultConfig().
// Returns an error if the file exists but cannot be parsed.
func LoadConfig(dir string) (*BuildConfig, error) {
func LoadConfig(fs io.Medium, dir string) (*BuildConfig, error) {
configPath := filepath.Join(dir, ConfigDir, ConfigFileName)
// Convert to absolute path for io.Local
absPath, err := filepath.Abs(configPath)
if err != nil {
return nil, fmt.Errorf("build.LoadConfig: failed to resolve path: %w", err)
}
content, err := io.Local.Read(absPath)
content, err := fs.Read(configPath)
if err != nil {
if os.IsNotExist(err) {
return DefaultConfig(), nil
@ -87,7 +81,8 @@ func LoadConfig(dir string) (*BuildConfig, error) {
}
var cfg BuildConfig
if err := yaml.Unmarshal([]byte(content), &cfg); err != nil {
data := []byte(content)
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("build.LoadConfig: failed to parse config file: %w", err)
}
@ -115,7 +110,6 @@ func DefaultConfig() *BuildConfig {
Targets: []TargetConfig{
{OS: "linux", Arch: "amd64"},
{OS: "linux", Arch: "arm64"},
{OS: "darwin", Arch: "amd64"},
{OS: "darwin", Arch: "arm64"},
{OS: "windows", Arch: "amd64"},
},
@ -161,8 +155,8 @@ func ConfigPath(dir string) string {
}
// ConfigExists checks if a build config file exists in the given directory.
func ConfigExists(dir string) bool {
return fileExists(ConfigPath(dir))
func ConfigExists(fs io.Medium, dir string) bool {
return fileExists(fs, ConfigPath(dir))
}
// ToTargets converts TargetConfig slice to Target slice for use with builders.

View file

@ -33,12 +33,12 @@ var markers = []projectMarker{
// 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).
// For example, a Wails project returns [wails, go] since it has both wails.json and go.mod.
func Discover(dir string) ([]ProjectType, error) {
func Discover(fs io.Medium, dir string) ([]ProjectType, error) {
var detected []ProjectType
for _, m := range markers {
path := filepath.Join(dir, m.file)
if fileExists(path) {
if fileExists(fs, path) {
// Avoid duplicates (shouldn't happen with current markers, but defensive)
if !slices.Contains(detected, m.projectType) {
detected = append(detected, m.projectType)
@ -51,8 +51,8 @@ func Discover(dir string) ([]ProjectType, error) {
// PrimaryType returns the most specific project type detected in the directory.
// Returns empty string if no project type is detected.
func PrimaryType(dir string) (ProjectType, error) {
types, err := Discover(dir)
func PrimaryType(fs io.Medium, dir string) (ProjectType, error) {
types, err := Discover(fs, dir)
if err != nil {
return "", err
}
@ -63,31 +63,27 @@ func PrimaryType(dir string) (ProjectType, error) {
}
// IsGoProject checks if the directory contains a Go project (go.mod or wails.json).
func IsGoProject(dir string) bool {
return fileExists(filepath.Join(dir, markerGoMod)) ||
fileExists(filepath.Join(dir, markerWails))
func IsGoProject(fs io.Medium, dir string) bool {
return fileExists(fs, filepath.Join(dir, markerGoMod)) ||
fileExists(fs, filepath.Join(dir, markerWails))
}
// IsWailsProject checks if the directory contains a Wails project.
func IsWailsProject(dir string) bool {
return fileExists(filepath.Join(dir, markerWails))
func IsWailsProject(fs io.Medium, dir string) bool {
return fileExists(fs, filepath.Join(dir, markerWails))
}
// IsNodeProject checks if the directory contains a Node.js project.
func IsNodeProject(dir string) bool {
return fileExists(filepath.Join(dir, markerNodePackage))
func IsNodeProject(fs io.Medium, dir string) bool {
return fileExists(fs, filepath.Join(dir, markerNodePackage))
}
// IsPHPProject checks if the directory contains a PHP project.
func IsPHPProject(dir string) bool {
return fileExists(filepath.Join(dir, markerComposer))
func IsPHPProject(fs io.Medium, dir string) bool {
return fileExists(fs, filepath.Join(dir, markerComposer))
}
// fileExists checks if a file exists and is not a directory.
func fileExists(path string) bool {
absPath, err := filepath.Abs(path)
if err != nil {
return false
}
return io.Local.IsFile(absPath)
func fileExists(fs io.Medium, path string) bool {
return fs.IsFile(path)
}

View file

@ -52,13 +52,6 @@ func TestDiscover_Good(t *testing.T) {
assert.Equal(t, []ProjectType{ProjectTypePHP}, types)
})
t.Run("detects C++ project", func(t *testing.T) {
dir := setupTestDir(t, "CMakeLists.txt")
types, err := Discover(fs, dir)
assert.NoError(t, err)
assert.Equal(t, []ProjectType{ProjectTypeCPP}, types)
})
t.Run("detects multiple project types", func(t *testing.T) {
dir := setupTestDir(t, "go.mod", "package.json")
types, err := Discover(fs, dir)
@ -162,19 +155,6 @@ func TestIsNodeProject_Good(t *testing.T) {
})
}
func TestIsCPPProject_Good(t *testing.T) {
fs := io.Local
t.Run("true with CMakeLists.txt", func(t *testing.T) {
dir := setupTestDir(t, "CMakeLists.txt")
assert.True(t, IsCPPProject(fs, dir))
})
t.Run("false without CMakeLists.txt", func(t *testing.T) {
dir := t.TempDir()
assert.False(t, IsCPPProject(fs, dir))
})
}
func TestIsPHPProject_Good(t *testing.T) {
fs := io.Local
t.Run("true with composer.json", func(t *testing.T) {
@ -229,7 +209,6 @@ func TestDiscover_Testdata(t *testing.T) {
{"wails-project", "wails-project", []ProjectType{ProjectTypeWails, ProjectTypeGo}},
{"node-project", "node-project", []ProjectType{ProjectTypeNode}},
{"php-project", "php-project", []ProjectType{ProjectTypePHP}},
{"cpp-project", "cpp-project", []ProjectType{ProjectTypeCPP}},
{"multi-project", "multi-project", []ProjectType{ProjectTypeGo, ProjectTypeNode}},
{"empty-project", "empty-project", []ProjectType{}},
}

View file

@ -25,6 +25,8 @@ type Release struct {
Changelog string
// ProjectDir is the root directory of the project.
ProjectDir string
// FS is the medium for file operations.
FS io.Medium
}
// Publish publishes pre-built artifacts from dist/ to configured targets.
@ -35,6 +37,8 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
return nil, fmt.Errorf("release.Publish: config is nil")
}
m := io.Local
projectDir := cfg.projectDir
if projectDir == "" {
projectDir = "."
@ -57,7 +61,7 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
// Step 2: Find pre-built artifacts in dist/
distDir := filepath.Join(absProjectDir, "dist")
artifacts, err := findArtifacts(distDir)
artifacts, err := findArtifacts(m, distDir)
if err != nil {
return nil, fmt.Errorf("release.Publish: %w", err)
}
@ -78,11 +82,12 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
Artifacts: artifacts,
Changelog: changelog,
ProjectDir: absProjectDir,
FS: m,
}
// Step 4: Publish to configured targets
if len(cfg.Publishers) > 0 {
pubRelease := publishers.NewRelease(release.Version, release.Artifacts, release.Changelog, release.ProjectDir)
pubRelease := publishers.NewRelease(release.Version, release.Artifacts, release.Changelog, release.ProjectDir, release.FS)
for _, pubCfg := range cfg.Publishers {
publisher, err := getPublisher(pubCfg.Type)
@ -102,14 +107,14 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
}
// findArtifacts discovers pre-built artifacts in the dist directory.
func findArtifacts(distDir string) ([]build.Artifact, error) {
if !io.Local.IsDir(distDir) {
func findArtifacts(m io.Medium, distDir string) ([]build.Artifact, error) {
if !m.IsDir(distDir) {
return nil, fmt.Errorf("dist/ directory not found")
}
var artifacts []build.Artifact
entries, err := io.Local.List(distDir)
entries, err := m.List(distDir)
if err != nil {
return nil, fmt.Errorf("failed to read dist/: %w", err)
}
@ -143,6 +148,8 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
return nil, fmt.Errorf("release.Run: config is nil")
}
m := io.Local
projectDir := cfg.projectDir
if projectDir == "" {
projectDir = "."
@ -171,7 +178,7 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
}
// Step 3: Build artifacts
artifacts, err := buildArtifacts(ctx, cfg, absProjectDir, version)
artifacts, err := buildArtifacts(ctx, m, cfg, absProjectDir, version)
if err != nil {
return nil, fmt.Errorf("release.Run: build failed: %w", err)
}
@ -181,12 +188,13 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
Artifacts: artifacts,
Changelog: changelog,
ProjectDir: absProjectDir,
FS: m,
}
// Step 4: Publish to configured targets
if len(cfg.Publishers) > 0 {
// Convert to publisher types
pubRelease := publishers.NewRelease(release.Version, release.Artifacts, release.Changelog, release.ProjectDir)
pubRelease := publishers.NewRelease(release.Version, release.Artifacts, release.Changelog, release.ProjectDir, release.FS)
for _, pubCfg := range cfg.Publishers {
publisher, err := getPublisher(pubCfg.Type)
@ -207,9 +215,9 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
}
// buildArtifacts builds all artifacts for the release.
func buildArtifacts(ctx context.Context, cfg *Config, projectDir, version string) ([]build.Artifact, error) {
func buildArtifacts(ctx context.Context, fs io.Medium, cfg *Config, projectDir, version string) ([]build.Artifact, error) {
// Load build configuration
buildCfg, err := build.LoadConfig(projectDir)
buildCfg, err := build.LoadConfig(fs, projectDir)
if err != nil {
return nil, fmt.Errorf("failed to load build config: %w", err)
}
@ -227,7 +235,6 @@ func buildArtifacts(ctx context.Context, cfg *Config, projectDir, version string
targets = []build.Target{
{OS: "linux", Arch: "amd64"},
{OS: "linux", Arch: "arm64"},
{OS: "darwin", Arch: "amd64"},
{OS: "darwin", Arch: "arm64"},
{OS: "windows", Arch: "amd64"},
}
@ -249,7 +256,7 @@ func buildArtifacts(ctx context.Context, cfg *Config, projectDir, version string
outputDir := filepath.Join(projectDir, "dist")
// Get builder (detect project type)
projectType, err := build.PrimaryType(projectDir)
projectType, err := build.PrimaryType(fs, projectDir)
if err != nil {
return nil, fmt.Errorf("failed to detect project type: %w", err)
}
@ -261,6 +268,7 @@ func buildArtifacts(ctx context.Context, cfg *Config, projectDir, version string
// Build configuration
buildConfig := &build.Config{
FS: fs,
ProjectDir: projectDir,
OutputDir: outputDir,
Name: binaryName,
@ -275,20 +283,20 @@ func buildArtifacts(ctx context.Context, cfg *Config, projectDir, version string
}
// Archive artifacts
archivedArtifacts, err := build.ArchiveAll(artifacts)
archivedArtifacts, err := build.ArchiveAll(fs, artifacts)
if err != nil {
return nil, fmt.Errorf("archive failed: %w", err)
}
// Compute checksums
checksummedArtifacts, err := build.ChecksumAll(archivedArtifacts)
checksummedArtifacts, err := build.ChecksumAll(fs, archivedArtifacts)
if err != nil {
return nil, fmt.Errorf("checksum failed: %w", err)
}
// Write CHECKSUMS.txt
checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt")
if err := build.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil {
if err := build.WriteChecksumFile(fs, checksummedArtifacts, checksumPath); err != nil {
return nil, fmt.Errorf("failed to write checksums file: %w", err)
}
@ -309,7 +317,7 @@ func getBuilder(projectType build.ProjectType) (build.Builder, error) {
case build.ProjectTypeGo:
return builders.NewGoBuilder(), nil
case build.ProjectTypeNode:
return nil, fmt.Errorf("Node.js builder not yet implemented")
return nil, fmt.Errorf("node.js builder not yet implemented")
case build.ProjectTypePHP:
return nil, fmt.Errorf("PHP builder not yet implemented")
default: