diff --git a/pkg/build/checksum.go b/pkg/build/checksum.go index d9aa240..6610edf 100644 --- a/pkg/build/checksum.go +++ b/pkg/build/checksum.go @@ -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) } diff --git a/pkg/build/config.go b/pkg/build/config.go index 365dad4..c777b69 100644 --- a/pkg/build/config.go +++ b/pkg/build/config.go @@ -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. diff --git a/pkg/build/discovery.go b/pkg/build/discovery.go index 8e9748c..ea4ee12 100644 --- a/pkg/build/discovery.go +++ b/pkg/build/discovery.go @@ -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) } diff --git a/pkg/build/discovery_test.go b/pkg/build/discovery_test.go index 11b4cc6..414b1a3 100644 --- a/pkg/build/discovery_test.go +++ b/pkg/build/discovery_test.go @@ -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{}}, } diff --git a/pkg/release/release.go b/pkg/release/release.go index f5dd53b..7237ffd 100644 --- a/pkg/release/release.go +++ b/pkg/release/release.go @@ -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: