chore(io): Migrate pkg/build to Medium abstraction
- Updated io.Medium interface with Open() and Create() methods to support streaming. - Migrated pkg/build, pkg/build/builders, and pkg/build/signing to use io.Medium. - Added FS field to build.Config and updated build.Builder interface. - Refactored checksum and archive logic to use io.Medium streaming. - Updated pkg/release and pkg/build/buildcmd to use io.Local. - Updated unit tests to match new signatures.
This commit is contained in:
parent
b806f4f11c
commit
7c25034261
28 changed files with 456 additions and 267 deletions
|
|
@ -8,11 +8,11 @@ import (
|
|||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
io_interface "github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// ArchiveFormat specifies the compression format for archives.
|
||||
|
|
@ -31,28 +31,28 @@ const (
|
|||
// Uses tar.gz for linux/darwin and zip for windows.
|
||||
// The archive is created alongside the binary (e.g., dist/myapp_linux_amd64.tar.gz).
|
||||
// Returns a new Artifact with Path pointing to the archive.
|
||||
func Archive(artifact Artifact) (Artifact, error) {
|
||||
return ArchiveWithFormat(artifact, ArchiveFormatGzip)
|
||||
func Archive(fs io_interface.Medium, artifact Artifact) (Artifact, error) {
|
||||
return ArchiveWithFormat(fs, artifact, ArchiveFormatGzip)
|
||||
}
|
||||
|
||||
// ArchiveXZ creates an archive for a single artifact using xz compression.
|
||||
// Uses tar.xz for linux/darwin and zip for windows.
|
||||
// Returns a new Artifact with Path pointing to the archive.
|
||||
func ArchiveXZ(artifact Artifact) (Artifact, error) {
|
||||
return ArchiveWithFormat(artifact, ArchiveFormatXZ)
|
||||
func ArchiveXZ(fs io_interface.Medium, artifact Artifact) (Artifact, error) {
|
||||
return ArchiveWithFormat(fs, artifact, ArchiveFormatXZ)
|
||||
}
|
||||
|
||||
// ArchiveWithFormat creates an archive for a single artifact with the specified format.
|
||||
// Uses tar.gz or tar.xz for linux/darwin and zip for windows.
|
||||
// The archive is created alongside the binary (e.g., dist/myapp_linux_amd64.tar.xz).
|
||||
// Returns a new Artifact with Path pointing to the archive.
|
||||
func ArchiveWithFormat(artifact Artifact, format ArchiveFormat) (Artifact, error) {
|
||||
func ArchiveWithFormat(fs io_interface.Medium, artifact Artifact, format ArchiveFormat) (Artifact, error) {
|
||||
if artifact.Path == "" {
|
||||
return Artifact{}, fmt.Errorf("build.Archive: artifact path is empty")
|
||||
}
|
||||
|
||||
// Verify the source file exists
|
||||
info, err := os.Stat(artifact.Path)
|
||||
info, err := fs.Stat(artifact.Path)
|
||||
if err != nil {
|
||||
return Artifact{}, fmt.Errorf("build.Archive: source file not found: %w", err)
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ func ArchiveWithFormat(artifact Artifact, format ArchiveFormat) (Artifact, error
|
|||
|
||||
// Determine archive type based on OS and format
|
||||
var archivePath string
|
||||
var archiveFunc func(src, dst string) error
|
||||
var archiveFunc func(fs io_interface.Medium, src, dst string) error
|
||||
|
||||
if artifact.OS == "windows" {
|
||||
archivePath = archiveFilename(artifact, ".zip")
|
||||
|
|
@ -79,7 +79,7 @@ func ArchiveWithFormat(artifact Artifact, format ArchiveFormat) (Artifact, error
|
|||
}
|
||||
|
||||
// Create the archive
|
||||
if err := archiveFunc(artifact.Path, archivePath); err != nil {
|
||||
if err := archiveFunc(fs, artifact.Path, archivePath); err != nil {
|
||||
return Artifact{}, fmt.Errorf("build.Archive: failed to create archive: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -93,26 +93,26 @@ func ArchiveWithFormat(artifact Artifact, format ArchiveFormat) (Artifact, error
|
|||
|
||||
// ArchiveAll archives all artifacts using gzip compression.
|
||||
// Returns a slice of new artifacts pointing to the archives.
|
||||
func ArchiveAll(artifacts []Artifact) ([]Artifact, error) {
|
||||
return ArchiveAllWithFormat(artifacts, ArchiveFormatGzip)
|
||||
func ArchiveAll(fs io_interface.Medium, artifacts []Artifact) ([]Artifact, error) {
|
||||
return ArchiveAllWithFormat(fs, artifacts, ArchiveFormatGzip)
|
||||
}
|
||||
|
||||
// ArchiveAllXZ archives all artifacts using xz compression.
|
||||
// Returns a slice of new artifacts pointing to the archives.
|
||||
func ArchiveAllXZ(artifacts []Artifact) ([]Artifact, error) {
|
||||
return ArchiveAllWithFormat(artifacts, ArchiveFormatXZ)
|
||||
func ArchiveAllXZ(fs io_interface.Medium, artifacts []Artifact) ([]Artifact, error) {
|
||||
return ArchiveAllWithFormat(fs, artifacts, ArchiveFormatXZ)
|
||||
}
|
||||
|
||||
// ArchiveAllWithFormat archives all artifacts with the specified format.
|
||||
// Returns a slice of new artifacts pointing to the archives.
|
||||
func ArchiveAllWithFormat(artifacts []Artifact, format ArchiveFormat) ([]Artifact, error) {
|
||||
func ArchiveAllWithFormat(fs io_interface.Medium, artifacts []Artifact, format ArchiveFormat) ([]Artifact, error) {
|
||||
if len(artifacts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var archived []Artifact
|
||||
for _, artifact := range artifacts {
|
||||
arch, err := ArchiveWithFormat(artifact, format)
|
||||
arch, err := ArchiveWithFormat(fs, artifact, format)
|
||||
if err != nil {
|
||||
return archived, fmt.Errorf("build.ArchiveAll: failed to archive %s: %w", artifact.Path, err)
|
||||
}
|
||||
|
|
@ -142,9 +142,9 @@ func archiveFilename(artifact Artifact, ext string) string {
|
|||
|
||||
// createTarXzArchive creates a tar.xz archive containing a single file.
|
||||
// Uses Borg's compress package for xz compression.
|
||||
func createTarXzArchive(src, dst string) error {
|
||||
func createTarXzArchive(fs io_interface.Medium, src, dst string) error {
|
||||
// Open the source file
|
||||
srcFile, err := os.Open(src)
|
||||
srcFile, err := fs.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file: %w", err)
|
||||
}
|
||||
|
|
@ -185,7 +185,7 @@ func createTarXzArchive(src, dst string) error {
|
|||
}
|
||||
|
||||
// Write to destination file
|
||||
if err := os.WriteFile(dst, xzData, 0644); err != nil {
|
||||
if err := fs.Write(dst, string(xzData)); err != nil {
|
||||
return fmt.Errorf("failed to write archive file: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -193,9 +193,9 @@ func createTarXzArchive(src, dst string) error {
|
|||
}
|
||||
|
||||
// createTarGzArchive creates a tar.gz archive containing a single file.
|
||||
func createTarGzArchive(src, dst string) error {
|
||||
func createTarGzArchive(fs io_interface.Medium, src, dst string) error {
|
||||
// Open the source file
|
||||
srcFile, err := os.Open(src)
|
||||
srcFile, err := fs.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file: %w", err)
|
||||
}
|
||||
|
|
@ -207,7 +207,7 @@ func createTarGzArchive(src, dst string) error {
|
|||
}
|
||||
|
||||
// Create the destination file
|
||||
dstFile, err := os.Create(dst)
|
||||
dstFile, err := fs.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create archive file: %w", err)
|
||||
}
|
||||
|
|
@ -243,9 +243,9 @@ func createTarGzArchive(src, dst string) error {
|
|||
}
|
||||
|
||||
// createZipArchive creates a zip archive containing a single file.
|
||||
func createZipArchive(src, dst string) error {
|
||||
func createZipArchive(fs io_interface.Medium, src, dst string) error {
|
||||
// Open the source file
|
||||
srcFile, err := os.Open(src)
|
||||
srcFile, err := fs.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file: %w", err)
|
||||
}
|
||||
|
|
@ -257,7 +257,7 @@ func createZipArchive(src, dst string) error {
|
|||
}
|
||||
|
||||
// Create the destination file
|
||||
dstFile, err := os.Create(dst)
|
||||
dstFile, err := fs.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create archive file: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
io_interface "github.com/host-uk/core/pkg/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -37,6 +38,7 @@ func setupArchiveTestFile(t *testing.T, name, os_, arch string) (binaryPath stri
|
|||
}
|
||||
|
||||
func TestArchive_Good(t *testing.T) {
|
||||
fs := io_interface.Local
|
||||
t.Run("creates tar.gz for linux", func(t *testing.T) {
|
||||
binaryPath, outputDir := setupArchiveTestFile(t, "myapp", "linux", "amd64")
|
||||
|
||||
|
|
@ -46,7 +48,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := Archive(artifact)
|
||||
result, err := Archive(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify archive was created
|
||||
|
|
@ -71,7 +73,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
Arch: "arm64",
|
||||
}
|
||||
|
||||
result, err := Archive(artifact)
|
||||
result, err := Archive(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPath := filepath.Join(outputDir, "myapp_darwin_arm64.tar.gz")
|
||||
|
|
@ -90,7 +92,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := Archive(artifact)
|
||||
result, err := Archive(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Windows archives should strip .exe from archive name
|
||||
|
|
@ -111,7 +113,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
Checksum: "abc123",
|
||||
}
|
||||
|
||||
result, err := Archive(artifact)
|
||||
result, err := Archive(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "abc123", result.Checksum)
|
||||
})
|
||||
|
|
@ -125,7 +127,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := ArchiveXZ(artifact)
|
||||
result, err := ArchiveXZ(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPath := filepath.Join(outputDir, "myapp_linux_amd64.tar.xz")
|
||||
|
|
@ -144,7 +146,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
Arch: "arm64",
|
||||
}
|
||||
|
||||
result, err := ArchiveWithFormat(artifact, ArchiveFormatXZ)
|
||||
result, err := ArchiveWithFormat(fs, artifact, ArchiveFormatXZ)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPath := filepath.Join(outputDir, "myapp_darwin_arm64.tar.xz")
|
||||
|
|
@ -163,7 +165,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := ArchiveWithFormat(artifact, ArchiveFormatXZ)
|
||||
result, err := ArchiveWithFormat(fs, artifact, ArchiveFormatXZ)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Windows should still get .zip regardless of format
|
||||
|
|
@ -176,6 +178,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestArchive_Bad(t *testing.T) {
|
||||
fs := io_interface.Local
|
||||
t.Run("returns error for empty path", func(t *testing.T) {
|
||||
artifact := Artifact{
|
||||
Path: "",
|
||||
|
|
@ -183,7 +186,7 @@ func TestArchive_Bad(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := Archive(artifact)
|
||||
result, err := Archive(fs, artifact)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "artifact path is empty")
|
||||
assert.Empty(t, result.Path)
|
||||
|
|
@ -196,7 +199,7 @@ func TestArchive_Bad(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := Archive(artifact)
|
||||
result, err := Archive(fs, artifact)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "source file not found")
|
||||
assert.Empty(t, result.Path)
|
||||
|
|
@ -211,7 +214,7 @@ func TestArchive_Bad(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := Archive(artifact)
|
||||
result, err := Archive(fs, artifact)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "source path is a directory")
|
||||
assert.Empty(t, result.Path)
|
||||
|
|
@ -219,6 +222,7 @@ func TestArchive_Bad(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestArchiveAll_Good(t *testing.T) {
|
||||
fs := io_interface.Local
|
||||
t.Run("archives multiple artifacts", func(t *testing.T) {
|
||||
outputDir := t.TempDir()
|
||||
|
||||
|
|
@ -255,7 +259,7 @@ func TestArchiveAll_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
results, err := ArchiveAll(artifacts)
|
||||
results, err := ArchiveAll(fs, artifacts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 4)
|
||||
|
||||
|
|
@ -268,19 +272,20 @@ func TestArchiveAll_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("returns nil for empty slice", func(t *testing.T) {
|
||||
results, err := ArchiveAll([]Artifact{})
|
||||
results, err := ArchiveAll(fs, []Artifact{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, results)
|
||||
})
|
||||
|
||||
t.Run("returns nil for nil slice", func(t *testing.T) {
|
||||
results, err := ArchiveAll(nil)
|
||||
results, err := ArchiveAll(fs, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, results)
|
||||
})
|
||||
}
|
||||
|
||||
func TestArchiveAll_Bad(t *testing.T) {
|
||||
fs := io_interface.Local
|
||||
t.Run("returns partial results on error", func(t *testing.T) {
|
||||
binaryPath, _ := setupArchiveTestFile(t, "myapp", "linux", "amd64")
|
||||
|
||||
|
|
@ -289,7 +294,7 @@ func TestArchiveAll_Bad(t *testing.T) {
|
|||
{Path: "/nonexistent/binary", OS: "linux", Arch: "arm64"}, // This will fail
|
||||
}
|
||||
|
||||
results, err := ArchiveAll(artifacts)
|
||||
results, err := ArchiveAll(fs, artifacts)
|
||||
assert.Error(t, err)
|
||||
// Should have the first successful result
|
||||
assert.Len(t, results, 1)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ package build
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// ProjectType represents a detected project type.
|
||||
|
|
@ -49,6 +51,8 @@ type Artifact struct {
|
|||
|
||||
// Config holds build configuration.
|
||||
type Config struct {
|
||||
// FS is the medium used for file operations.
|
||||
FS io.Medium
|
||||
// ProjectDir is the root directory of the project.
|
||||
ProjectDir string
|
||||
// OutputDir is where build artifacts are placed.
|
||||
|
|
@ -78,7 +82,7 @@ type Builder interface {
|
|||
// Name returns the builder's identifier.
|
||||
Name() string
|
||||
// Detect checks if this builder can handle the project in the given directory.
|
||||
Detect(dir string) (bool, error)
|
||||
Detect(fs io.Medium, dir string) (bool, error)
|
||||
// Build compiles the project for the specified targets.
|
||||
Build(ctx context.Context, cfg *Config, targets []Target) ([]Artifact, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,14 @@ import (
|
|||
"github.com/host-uk/core/pkg/build/builders"
|
||||
"github.com/host-uk/core/pkg/build/signing"
|
||||
"github.com/host-uk/core/pkg/i18n"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// runProjectBuild handles the main `core build` command with auto-detection.
|
||||
func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targetsFlag string, outputDir string, doArchive bool, doChecksum bool, configPath string, format string, push bool, imageName string, noSign bool, notarize bool, verbose bool) error {
|
||||
// Use local filesystem as the default medium
|
||||
fs := io.Local
|
||||
|
||||
// Get current working directory as project root
|
||||
projectDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
|
|
@ -29,7 +33,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
}
|
||||
|
||||
// Load configuration from .core/build.yaml (or defaults)
|
||||
buildCfg, err := build.LoadConfig(projectDir)
|
||||
buildCfg, err := build.LoadConfig(fs, projectDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load config"}), err)
|
||||
}
|
||||
|
|
@ -39,7 +43,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
if buildType != "" {
|
||||
projectType = build.ProjectType(buildType)
|
||||
} else {
|
||||
projectType, err = build.PrimaryType(projectDir)
|
||||
projectType, err = build.PrimaryType(fs, projectDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "detect project type"}), err)
|
||||
}
|
||||
|
|
@ -98,6 +102,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
|
||||
// Create build config for the builder
|
||||
cfg := &build.Config{
|
||||
FS: fs,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: binaryName,
|
||||
|
|
@ -161,7 +166,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
signingArtifacts[i] = signing.Artifact{Path: a.Path, OS: a.OS, Arch: a.Arch}
|
||||
}
|
||||
|
||||
if err := signing.SignBinaries(ctx, signCfg, signingArtifacts); err != nil {
|
||||
if err := signing.SignBinaries(ctx, fs, signCfg, signingArtifacts); err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.signing_failed"), err)
|
||||
}
|
||||
|
|
@ -169,7 +174,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
}
|
||||
|
||||
if signCfg.MacOS.Notarize {
|
||||
if err := signing.NotarizeBinaries(ctx, signCfg, signingArtifacts); err != nil {
|
||||
if err := signing.NotarizeBinaries(ctx, fs, signCfg, signingArtifacts); err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.notarization_failed"), err)
|
||||
}
|
||||
|
|
@ -186,7 +191,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.archive")), i18n.T("cmd.build.creating_archives"))
|
||||
}
|
||||
|
||||
archivedArtifacts, err = build.ArchiveAll(artifacts)
|
||||
archivedArtifacts, err = build.ArchiveAll(fs, artifacts)
|
||||
if err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.archive_failed"), err)
|
||||
|
|
@ -256,12 +261,13 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
|
||||
// computeAndWriteChecksums computes checksums for artifacts and writes CHECKSUMS.txt.
|
||||
func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []build.Artifact, signCfg signing.SignConfig, ciMode bool, verbose bool) ([]build.Artifact, error) {
|
||||
fs := io.Local
|
||||
if verbose && !ciMode {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.checksum")), i18n.T("cmd.build.computing_checksums"))
|
||||
}
|
||||
|
||||
checksummedArtifacts, err := build.ChecksumAll(artifacts)
|
||||
checksummedArtifacts, err := build.ChecksumAll(fs, artifacts)
|
||||
if err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.checksum_failed"), err)
|
||||
|
|
@ -271,7 +277,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
|
|||
|
||||
// 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 {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("common.error.failed", map[string]any{"Action": "write CHECKSUMS.txt"}), err)
|
||||
}
|
||||
|
|
@ -280,7 +286,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
|
|||
|
||||
// Sign checksums with GPG
|
||||
if signCfg.Enabled {
|
||||
if err := signing.SignChecksums(ctx, signCfg, checksumPath); err != nil {
|
||||
if err := signing.SignChecksums(ctx, fs, signCfg, checksumPath); err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.gpg_signing_failed"), err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// DockerBuilder builds Docker images.
|
||||
|
|
@ -26,9 +27,9 @@ func (b *DockerBuilder) Name() string {
|
|||
}
|
||||
|
||||
// Detect checks if a Dockerfile exists in the directory.
|
||||
func (b *DockerBuilder) Detect(dir string) (bool, error) {
|
||||
func (b *DockerBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
dockerfilePath := filepath.Join(dir, "Dockerfile")
|
||||
if _, err := os.Stat(dockerfilePath); err == nil {
|
||||
if fs.IsFile(dockerfilePath) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
|
|
@ -53,7 +54,7 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
}
|
||||
|
||||
// Validate Dockerfile exists
|
||||
if _, err := os.Stat(dockerfile); err != nil {
|
||||
if !cfg.FS.IsFile(dockerfile) {
|
||||
return nil, fmt.Errorf("docker.Build: Dockerfile not found: %s", dockerfile)
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +151,7 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
args = append(args, cfg.ProjectDir)
|
||||
|
||||
// Create output directory
|
||||
if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
|
||||
if err := cfg.FS.EnsureDir(cfg.OutputDir); err != nil {
|
||||
return nil, fmt.Errorf("docker.Build: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// GoBuilder implements the Builder interface for Go projects.
|
||||
|
|
@ -27,8 +28,8 @@ func (b *GoBuilder) Name() string {
|
|||
|
||||
// Detect checks if this builder can handle the project in the given directory.
|
||||
// Uses IsGoProject from the build package which checks for go.mod or wails.json.
|
||||
func (b *GoBuilder) Detect(dir string) (bool, error) {
|
||||
return build.IsGoProject(dir), nil
|
||||
func (b *GoBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
return build.IsGoProject(fs, dir), nil
|
||||
}
|
||||
|
||||
// Build compiles the Go project for the specified targets.
|
||||
|
|
@ -44,7 +45,7 @@ func (b *GoBuilder) Build(ctx context.Context, cfg *build.Config, targets []buil
|
|||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
|
||||
if err := cfg.FS.EnsureDir(cfg.OutputDir); err != nil {
|
||||
return nil, fmt.Errorf("builders.GoBuilder.Build: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ func (b *GoBuilder) buildTarget(ctx context.Context, cfg *build.Config, target b
|
|||
|
||||
// Create platform-specific output path: output/os_arch/binary
|
||||
platformDir := filepath.Join(cfg.OutputDir, fmt.Sprintf("%s_%s", target.OS, target.Arch))
|
||||
if err := os.MkdirAll(platformDir, 0755); err != nil {
|
||||
if err := cfg.FS.EnsureDir(platformDir); err != nil {
|
||||
return build.Artifact{}, fmt.Errorf("failed to create platform directory: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -44,13 +45,14 @@ func TestGoBuilder_Name_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGoBuilder_Detect_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("detects Go project with go.mod", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewGoBuilder()
|
||||
detected, err := builder.Detect(dir)
|
||||
detected, err := builder.Detect(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, detected)
|
||||
})
|
||||
|
|
@ -61,7 +63,7 @@ func TestGoBuilder_Detect_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
builder := NewGoBuilder()
|
||||
detected, err := builder.Detect(dir)
|
||||
detected, err := builder.Detect(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, detected)
|
||||
})
|
||||
|
|
@ -73,7 +75,7 @@ func TestGoBuilder_Detect_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
builder := NewGoBuilder()
|
||||
detected, err := builder.Detect(dir)
|
||||
detected, err := builder.Detect(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, detected)
|
||||
})
|
||||
|
|
@ -82,7 +84,7 @@ func TestGoBuilder_Detect_Good(t *testing.T) {
|
|||
dir := t.TempDir()
|
||||
|
||||
builder := NewGoBuilder()
|
||||
detected, err := builder.Detect(dir)
|
||||
detected, err := builder.Detect(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, detected)
|
||||
})
|
||||
|
|
@ -99,6 +101,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "testbinary",
|
||||
|
|
@ -133,6 +136,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "multitest",
|
||||
|
|
@ -160,6 +164,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "wintest",
|
||||
|
|
@ -183,6 +188,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "", // Empty name
|
||||
|
|
@ -209,6 +215,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "ldflagstest",
|
||||
|
|
@ -230,6 +237,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "nestedtest",
|
||||
|
|
@ -261,6 +269,7 @@ func TestGoBuilder_Build_Bad(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: t.TempDir(),
|
||||
Name: "test",
|
||||
|
|
@ -279,6 +288,7 @@ func TestGoBuilder_Build_Bad(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: "/nonexistent/path",
|
||||
OutputDir: t.TempDir(),
|
||||
Name: "test",
|
||||
|
|
@ -309,6 +319,7 @@ func TestGoBuilder_Build_Bad(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: dir,
|
||||
OutputDir: t.TempDir(),
|
||||
Name: "test",
|
||||
|
|
@ -335,6 +346,7 @@ func TestGoBuilder_Build_Bad(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "partialtest",
|
||||
|
|
@ -360,6 +372,7 @@ func TestGoBuilder_Build_Bad(t *testing.T) {
|
|||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: t.TempDir(),
|
||||
Name: "canceltest",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// LinuxKitBuilder builds LinuxKit images.
|
||||
|
|
@ -26,14 +27,22 @@ func (b *LinuxKitBuilder) Name() string {
|
|||
}
|
||||
|
||||
// Detect checks if a linuxkit.yml or .yml config exists in the directory.
|
||||
func (b *LinuxKitBuilder) Detect(dir string) (bool, error) {
|
||||
func (b *LinuxKitBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
// Check for linuxkit.yml
|
||||
if _, err := os.Stat(filepath.Join(dir, "linuxkit.yml")); err == nil {
|
||||
if fs.IsFile(filepath.Join(dir, "linuxkit.yml")) {
|
||||
return true, nil
|
||||
}
|
||||
// Check for .core/linuxkit/*.yml
|
||||
if matches, _ := filepath.Glob(filepath.Join(dir, ".core", "linuxkit", "*.yml")); len(matches) > 0 {
|
||||
return true, nil
|
||||
// Check for .core/linuxkit/
|
||||
lkDir := filepath.Join(dir, ".core", "linuxkit")
|
||||
if fs.IsDir(lkDir) {
|
||||
entries, err := fs.List(lkDir)
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yml") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -49,13 +58,21 @@ func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
configPath := cfg.LinuxKitConfig
|
||||
if configPath == "" {
|
||||
// Auto-detect
|
||||
if _, err := os.Stat(filepath.Join(cfg.ProjectDir, "linuxkit.yml")); err == nil {
|
||||
if cfg.FS.IsFile(filepath.Join(cfg.ProjectDir, "linuxkit.yml")) {
|
||||
configPath = filepath.Join(cfg.ProjectDir, "linuxkit.yml")
|
||||
} else {
|
||||
// Look in .core/linuxkit/
|
||||
matches, _ := filepath.Glob(filepath.Join(cfg.ProjectDir, ".core", "linuxkit", "*.yml"))
|
||||
if len(matches) > 0 {
|
||||
configPath = matches[0]
|
||||
lkDir := filepath.Join(cfg.ProjectDir, ".core", "linuxkit")
|
||||
if cfg.FS.IsDir(lkDir) {
|
||||
entries, err := cfg.FS.List(lkDir)
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yml") {
|
||||
configPath = filepath.Join(lkDir, entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +82,7 @@ func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
}
|
||||
|
||||
// Validate config file exists
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
if !cfg.FS.IsFile(configPath) {
|
||||
return nil, fmt.Errorf("linuxkit.Build: config file not found: %s", configPath)
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +97,7 @@ func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
if outputDir == "" {
|
||||
outputDir = filepath.Join(cfg.ProjectDir, "dist")
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
if err := cfg.FS.EnsureDir(outputDir); err != nil {
|
||||
return nil, fmt.Errorf("linuxkit.Build: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -125,9 +142,9 @@ func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
artifactPath := b.getArtifactPath(outputDir, outputName, format)
|
||||
|
||||
// Verify the artifact was created
|
||||
if _, err := os.Stat(artifactPath); err != nil {
|
||||
if !cfg.FS.Exists(artifactPath) {
|
||||
// Try alternate naming conventions
|
||||
artifactPath = b.findArtifact(outputDir, outputName, format)
|
||||
artifactPath = b.findArtifact(cfg.FS, outputDir, outputName, format)
|
||||
if artifactPath == "" {
|
||||
return nil, fmt.Errorf("linuxkit.Build: artifact not found after build: expected %s", b.getArtifactPath(outputDir, outputName, format))
|
||||
}
|
||||
|
|
@ -175,7 +192,7 @@ func (b *LinuxKitBuilder) getArtifactPath(outputDir, outputName, format string)
|
|||
}
|
||||
|
||||
// findArtifact searches for the built artifact with various naming conventions.
|
||||
func (b *LinuxKitBuilder) findArtifact(outputDir, outputName, format string) string {
|
||||
func (b *LinuxKitBuilder) findArtifact(fs io.Medium, outputDir, outputName, format string) string {
|
||||
// LinuxKit can create files with different suffixes
|
||||
extensions := []string{
|
||||
b.getFormatExtension(format),
|
||||
|
|
@ -185,18 +202,23 @@ func (b *LinuxKitBuilder) findArtifact(outputDir, outputName, format string) str
|
|||
|
||||
for _, ext := range extensions {
|
||||
path := filepath.Join(outputDir, outputName+ext)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
if fs.Exists(path) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find any file matching the output name
|
||||
matches, _ := filepath.Glob(filepath.Join(outputDir, outputName+"*"))
|
||||
for _, match := range matches {
|
||||
// Return first match that looks like an image
|
||||
ext := filepath.Ext(match)
|
||||
if ext == ".iso" || ext == ".qcow2" || ext == ".raw" || ext == ".vmdk" || ext == ".vhd" {
|
||||
return match
|
||||
entries, err := fs.List(outputDir)
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), outputName) {
|
||||
match := filepath.Join(outputDir, entry.Name())
|
||||
// Return first match that looks like an image
|
||||
ext := filepath.Ext(match)
|
||||
if ext == ".iso" || ext == ".qcow2" || ext == ".raw" || ext == ".vmdk" || ext == ".vhd" {
|
||||
return match
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// TaskfileBuilder builds projects using Taskfile (https://taskfile.dev/).
|
||||
|
|
@ -27,7 +28,7 @@ func (b *TaskfileBuilder) Name() string {
|
|||
}
|
||||
|
||||
// Detect checks if a Taskfile exists in the directory.
|
||||
func (b *TaskfileBuilder) Detect(dir string) (bool, error) {
|
||||
func (b *TaskfileBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
// Check for Taskfile.yml, Taskfile.yaml, or Taskfile
|
||||
taskfiles := []string{
|
||||
"Taskfile.yml",
|
||||
|
|
@ -38,7 +39,7 @@ func (b *TaskfileBuilder) Detect(dir string) (bool, error) {
|
|||
}
|
||||
|
||||
for _, tf := range taskfiles {
|
||||
if _, err := os.Stat(filepath.Join(dir, tf)); err == nil {
|
||||
if fs.IsFile(filepath.Join(dir, tf)) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -57,7 +58,7 @@ func (b *TaskfileBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
if outputDir == "" {
|
||||
outputDir = filepath.Join(cfg.ProjectDir, "dist")
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
if err := cfg.FS.EnsureDir(outputDir); err != nil {
|
||||
return nil, fmt.Errorf("taskfile.Build: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ func (b *TaskfileBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
}
|
||||
|
||||
// Try to find artifacts in output directory
|
||||
found := b.findArtifacts(outputDir)
|
||||
found := b.findArtifacts(cfg.FS, outputDir)
|
||||
artifacts = append(artifacts, found...)
|
||||
} else {
|
||||
// Run build task for each target
|
||||
|
|
@ -80,7 +81,7 @@ func (b *TaskfileBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
}
|
||||
|
||||
// Try to find artifacts for this target
|
||||
found := b.findArtifactsForTarget(outputDir, target)
|
||||
found := b.findArtifactsForTarget(cfg.FS, outputDir, target)
|
||||
artifacts = append(artifacts, found...)
|
||||
}
|
||||
}
|
||||
|
|
@ -147,10 +148,10 @@ func (b *TaskfileBuilder) runTask(ctx context.Context, cfg *build.Config, goos,
|
|||
}
|
||||
|
||||
// findArtifacts searches for built artifacts in the output directory.
|
||||
func (b *TaskfileBuilder) findArtifacts(outputDir string) []build.Artifact {
|
||||
func (b *TaskfileBuilder) findArtifacts(fs io.Medium, outputDir string) []build.Artifact {
|
||||
var artifacts []build.Artifact
|
||||
|
||||
entries, err := os.ReadDir(outputDir)
|
||||
entries, err := fs.List(outputDir)
|
||||
if err != nil {
|
||||
return artifacts
|
||||
}
|
||||
|
|
@ -177,13 +178,13 @@ func (b *TaskfileBuilder) findArtifacts(outputDir string) []build.Artifact {
|
|||
}
|
||||
|
||||
// findArtifactsForTarget searches for built artifacts for a specific target.
|
||||
func (b *TaskfileBuilder) findArtifactsForTarget(outputDir string, target build.Target) []build.Artifact {
|
||||
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 := filepath.Join(outputDir, fmt.Sprintf("%s_%s", target.OS, target.Arch))
|
||||
if info, err := os.Stat(platformSubdir); err == nil && info.IsDir() {
|
||||
entries, _ := os.ReadDir(platformSubdir)
|
||||
if fs.IsDir(platformSubdir) {
|
||||
entries, _ := fs.List(platformSubdir)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
// Handle .app bundles on macOS
|
||||
|
|
@ -219,18 +220,22 @@ func (b *TaskfileBuilder) findArtifactsForTarget(outputDir string, target build.
|
|||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches, _ := filepath.Glob(filepath.Join(outputDir, pattern))
|
||||
for _, match := range matches {
|
||||
info, err := os.Stat(match)
|
||||
if err != nil || info.IsDir() {
|
||||
continue
|
||||
}
|
||||
entries, _ := fs.List(outputDir)
|
||||
for _, entry := range entries {
|
||||
match := entry.Name()
|
||||
// Simple glob matching
|
||||
if b.matchPattern(match, pattern) {
|
||||
fullPath := filepath.Join(outputDir, match)
|
||||
if fs.IsDir(fullPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: match,
|
||||
OS: target.OS,
|
||||
Arch: target.Arch,
|
||||
})
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: fullPath,
|
||||
OS: target.OS,
|
||||
Arch: target.Arch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(artifacts) > 0 {
|
||||
|
|
@ -241,6 +246,12 @@ func (b *TaskfileBuilder) findArtifactsForTarget(outputDir string, target build.
|
|||
return artifacts
|
||||
}
|
||||
|
||||
// matchPattern implements a very simple glob matcher for Taskfile artifacts.
|
||||
func (b *TaskfileBuilder) matchPattern(name, pattern string) bool {
|
||||
p := strings.ReplaceAll(pattern, "*", "")
|
||||
return strings.Contains(name, p)
|
||||
}
|
||||
|
||||
// validateTaskCli checks if the task CLI is available.
|
||||
func (b *TaskfileBuilder) validateTaskCli() error {
|
||||
// Check PATH first
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ package builders
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// WailsBuilder implements the Builder interface for Wails v3 projects.
|
||||
|
|
@ -27,8 +27,8 @@ func (b *WailsBuilder) Name() string {
|
|||
|
||||
// Detect checks if this builder can handle the project in the given directory.
|
||||
// Uses IsWailsProject from the build package which checks for wails.json.
|
||||
func (b *WailsBuilder) Detect(dir string) (bool, error) {
|
||||
return build.IsWailsProject(dir), nil
|
||||
func (b *WailsBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
return build.IsWailsProject(fs, dir), nil
|
||||
}
|
||||
|
||||
// Build compiles the Wails project for the specified targets.
|
||||
|
|
@ -45,12 +45,12 @@ func (b *WailsBuilder) Build(ctx context.Context, cfg *build.Config, targets []b
|
|||
}
|
||||
|
||||
// Detect Wails version
|
||||
isV3 := b.isWailsV3(cfg.ProjectDir)
|
||||
isV3 := b.isWailsV3(cfg.FS, cfg.ProjectDir)
|
||||
|
||||
if isV3 {
|
||||
// Wails v3 strategy: Delegate to Taskfile
|
||||
taskBuilder := NewTaskfileBuilder()
|
||||
if detected, _ := taskBuilder.Detect(cfg.ProjectDir); detected {
|
||||
if detected, _ := taskBuilder.Detect(cfg.FS, cfg.ProjectDir); detected {
|
||||
return taskBuilder.Build(ctx, cfg, targets)
|
||||
}
|
||||
return nil, fmt.Errorf("wails v3 projects require a Taskfile for building")
|
||||
|
|
@ -58,7 +58,7 @@ func (b *WailsBuilder) Build(ctx context.Context, cfg *build.Config, targets []b
|
|||
|
||||
// Wails v2 strategy: Use 'wails build'
|
||||
// Ensure output directory exists
|
||||
if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
|
||||
if err := cfg.FS.EnsureDir(cfg.OutputDir); err != nil {
|
||||
return nil, fmt.Errorf("builders.WailsBuilder.Build: failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -78,13 +78,13 @@ func (b *WailsBuilder) Build(ctx context.Context, cfg *build.Config, targets []b
|
|||
}
|
||||
|
||||
// isWailsV3 checks if the project uses Wails v3 by inspecting go.mod.
|
||||
func (b *WailsBuilder) isWailsV3(dir string) bool {
|
||||
func (b *WailsBuilder) isWailsV3(fs io.Medium, dir string) bool {
|
||||
goModPath := filepath.Join(dir, "go.mod")
|
||||
data, err := os.ReadFile(goModPath)
|
||||
content, err := fs.Read(goModPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(data), "github.com/wailsapp/wails/v3")
|
||||
return strings.Contains(content, "github.com/wailsapp/wails/v3")
|
||||
}
|
||||
|
||||
// buildV2Target compiles for a single target platform using wails (v2).
|
||||
|
|
@ -123,7 +123,7 @@ func (b *WailsBuilder) buildV2Target(ctx context.Context, cfg *build.Config, tar
|
|||
wailsOutputDir := filepath.Join(cfg.ProjectDir, "build", "bin")
|
||||
|
||||
// Find the artifact in Wails output dir
|
||||
sourcePath, err := b.findArtifact(wailsOutputDir, binaryName, target)
|
||||
sourcePath, err := b.findArtifact(cfg.FS, wailsOutputDir, binaryName, target)
|
||||
if err != nil {
|
||||
return build.Artifact{}, fmt.Errorf("failed to find Wails v2 build artifact: %w", err)
|
||||
}
|
||||
|
|
@ -131,18 +131,18 @@ func (b *WailsBuilder) buildV2Target(ctx context.Context, cfg *build.Config, tar
|
|||
// Move/Copy to our output dir
|
||||
// Create platform specific dir in our output
|
||||
platformDir := filepath.Join(cfg.OutputDir, fmt.Sprintf("%s_%s", target.OS, target.Arch))
|
||||
if err := os.MkdirAll(platformDir, 0755); err != nil {
|
||||
if err := cfg.FS.EnsureDir(platformDir); err != nil {
|
||||
return build.Artifact{}, fmt.Errorf("failed to create output dir: %w", err)
|
||||
}
|
||||
|
||||
destPath := filepath.Join(platformDir, filepath.Base(sourcePath))
|
||||
|
||||
// Simple copy
|
||||
input, err := os.ReadFile(sourcePath)
|
||||
// Simple copy using the medium
|
||||
content, err := cfg.FS.Read(sourcePath)
|
||||
if err != nil {
|
||||
return build.Artifact{}, err
|
||||
}
|
||||
if err := os.WriteFile(destPath, input, 0755); err != nil {
|
||||
if err := cfg.FS.Write(destPath, content); err != nil {
|
||||
return build.Artifact{}, err
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ func (b *WailsBuilder) buildV2Target(ctx context.Context, cfg *build.Config, tar
|
|||
}
|
||||
|
||||
// findArtifact locates the built artifact based on the target platform.
|
||||
func (b *WailsBuilder) findArtifact(platformDir, binaryName string, target build.Target) (string, error) {
|
||||
func (b *WailsBuilder) findArtifact(fs io.Medium, platformDir, binaryName string, target build.Target) (string, error) {
|
||||
var candidates []string
|
||||
|
||||
switch target.OS {
|
||||
|
|
@ -181,13 +181,13 @@ func (b *WailsBuilder) findArtifact(platformDir, binaryName string, target build
|
|||
|
||||
// Try each candidate
|
||||
for _, candidate := range candidates {
|
||||
if fileOrDirExists(candidate) {
|
||||
if fs.Exists(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If no specific candidate found, try to find any executable or package in the directory
|
||||
entries, err := os.ReadDir(platformDir)
|
||||
entries, err := fs.List(platformDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read platform directory: %w", err)
|
||||
}
|
||||
|
|
@ -221,7 +221,7 @@ func (b *WailsBuilder) findArtifact(platformDir, binaryName string, target build
|
|||
|
||||
// detectPackageManager detects the frontend package manager based on lock files.
|
||||
// Returns "bun", "pnpm", "yarn", or "npm" (default).
|
||||
func detectPackageManager(dir string) string {
|
||||
func detectPackageManager(fs io.Medium, dir string) string {
|
||||
// Check in priority order: bun, pnpm, yarn, npm
|
||||
lockFiles := []struct {
|
||||
file string
|
||||
|
|
@ -234,7 +234,7 @@ func detectPackageManager(dir string) string {
|
|||
}
|
||||
|
||||
for _, lf := range lockFiles {
|
||||
if fileExists(filepath.Join(dir, lf.file)) {
|
||||
if fs.IsFile(filepath.Join(dir, lf.file)) {
|
||||
return lf.manager
|
||||
}
|
||||
}
|
||||
|
|
@ -243,20 +243,6 @@ func detectPackageManager(dir string) string {
|
|||
return "npm"
|
||||
}
|
||||
|
||||
// fileExists checks if a file exists and is not a directory.
|
||||
func fileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
// fileOrDirExists checks if a file or directory exists.
|
||||
func fileOrDirExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Ensure WailsBuilder implements the Builder interface.
|
||||
var _ build.Builder = (*WailsBuilder)(nil)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -91,6 +92,7 @@ func TestWailsBuilder_Build_Taskfile_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("delegates to Taskfile if present", func(t *testing.T) {
|
||||
fs := io.Local
|
||||
projectDir := setupWailsTestProject(t)
|
||||
outputDir := t.TempDir()
|
||||
|
||||
|
|
@ -107,6 +109,7 @@ tasks:
|
|||
|
||||
builder := NewWailsBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: fs,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "testapp",
|
||||
|
|
@ -136,11 +139,13 @@ func TestWailsBuilder_Build_V2_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("builds v2 project", func(t *testing.T) {
|
||||
fs := io.Local
|
||||
projectDir := setupWailsV2TestProject(t)
|
||||
outputDir := t.TempDir()
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: fs,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "testapp",
|
||||
|
|
@ -158,13 +163,14 @@ func TestWailsBuilder_Build_V2_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWailsBuilder_Detect_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("detects Wails project with wails.json", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "wails.json"), []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
detected, err := builder.Detect(dir)
|
||||
detected, err := builder.Detect(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, detected)
|
||||
})
|
||||
|
|
@ -175,7 +181,7 @@ func TestWailsBuilder_Detect_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
detected, err := builder.Detect(dir)
|
||||
detected, err := builder.Detect(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, detected)
|
||||
})
|
||||
|
|
@ -186,7 +192,7 @@ func TestWailsBuilder_Detect_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
detected, err := builder.Detect(dir)
|
||||
detected, err := builder.Detect(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, detected)
|
||||
})
|
||||
|
|
@ -195,19 +201,20 @@ func TestWailsBuilder_Detect_Good(t *testing.T) {
|
|||
dir := t.TempDir()
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
detected, err := builder.Detect(dir)
|
||||
detected, err := builder.Detect(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, detected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectPackageManager_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("detects bun from bun.lockb", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "bun.lockb"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := detectPackageManager(dir)
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "bun", result)
|
||||
})
|
||||
|
||||
|
|
@ -216,7 +223,7 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
err := os.WriteFile(filepath.Join(dir, "pnpm-lock.yaml"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := detectPackageManager(dir)
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "pnpm", result)
|
||||
})
|
||||
|
||||
|
|
@ -225,7 +232,7 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
err := os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := detectPackageManager(dir)
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "yarn", result)
|
||||
})
|
||||
|
||||
|
|
@ -234,14 +241,14 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
err := os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := detectPackageManager(dir)
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "npm", result)
|
||||
})
|
||||
|
||||
t.Run("defaults to npm when no lock file", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
result := detectPackageManager(dir)
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "npm", result)
|
||||
})
|
||||
|
||||
|
|
@ -252,7 +259,7 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte(""), 0644))
|
||||
|
||||
result := detectPackageManager(dir)
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "bun", result)
|
||||
})
|
||||
|
||||
|
|
@ -263,7 +270,7 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte(""), 0644))
|
||||
|
||||
result := detectPackageManager(dir)
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "pnpm", result)
|
||||
})
|
||||
|
||||
|
|
@ -273,7 +280,7 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte(""), 0644))
|
||||
|
||||
result := detectPackageManager(dir)
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "yarn", result)
|
||||
})
|
||||
}
|
||||
|
|
@ -293,6 +300,7 @@ func TestWailsBuilder_Build_Bad(t *testing.T) {
|
|||
|
||||
builder := NewWailsBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: t.TempDir(),
|
||||
Name: "test",
|
||||
|
|
@ -321,6 +329,7 @@ func TestWailsBuilder_Build_Good(t *testing.T) {
|
|||
|
||||
builder := NewWailsBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: "testapp",
|
||||
|
|
@ -359,6 +368,7 @@ func TestWailsBuilder_Ugly(t *testing.T) {
|
|||
|
||||
builder := NewWailsBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: dir,
|
||||
OutputDir: t.TempDir(),
|
||||
Name: "test",
|
||||
|
|
@ -386,6 +396,7 @@ func TestWailsBuilder_Ugly(t *testing.T) {
|
|||
|
||||
builder := NewWailsBuilder()
|
||||
cfg := &build.Config{
|
||||
FS: io.Local,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: t.TempDir(),
|
||||
Name: "canceltest",
|
||||
|
|
|
|||
|
|
@ -6,20 +6,21 @@ import (
|
|||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
io_interface "github.com/host-uk/core/pkg/io"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
@ -43,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)
|
||||
}
|
||||
|
|
@ -67,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
|
||||
}
|
||||
|
|
@ -87,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 := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("build.WriteChecksumFile: failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// Write the file
|
||||
if err := os.WriteFile(path, []byte(content), 0644); 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -23,6 +24,7 @@ func setupChecksumTestFile(t *testing.T, content string) string {
|
|||
}
|
||||
|
||||
func TestChecksum_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("computes SHA256 checksum", func(t *testing.T) {
|
||||
// Known SHA256 of "Hello, World!\n"
|
||||
path := setupChecksumTestFile(t, "Hello, World!\n")
|
||||
|
|
@ -34,7 +36,7 @@ func TestChecksum_Good(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := Checksum(artifact)
|
||||
result, err := Checksum(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedChecksum, result.Checksum)
|
||||
})
|
||||
|
|
@ -48,7 +50,7 @@ func TestChecksum_Good(t *testing.T) {
|
|||
Arch: "arm64",
|
||||
}
|
||||
|
||||
result, err := Checksum(artifact)
|
||||
result, err := Checksum(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, path, result.Path)
|
||||
|
|
@ -62,7 +64,7 @@ func TestChecksum_Good(t *testing.T) {
|
|||
|
||||
artifact := Artifact{Path: path, OS: "linux", Arch: "amd64"}
|
||||
|
||||
result, err := Checksum(artifact)
|
||||
result, err := Checksum(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
|
||||
// SHA256 produces 32 bytes = 64 hex characters
|
||||
|
|
@ -73,10 +75,10 @@ func TestChecksum_Good(t *testing.T) {
|
|||
path1 := setupChecksumTestFile(t, "content one")
|
||||
path2 := setupChecksumTestFile(t, "content two")
|
||||
|
||||
result1, err := Checksum(Artifact{Path: path1, OS: "linux", Arch: "amd64"})
|
||||
result1, err := Checksum(fs, Artifact{Path: path1, OS: "linux", Arch: "amd64"})
|
||||
require.NoError(t, err)
|
||||
|
||||
result2, err := Checksum(Artifact{Path: path2, OS: "linux", Arch: "amd64"})
|
||||
result2, err := Checksum(fs, Artifact{Path: path2, OS: "linux", Arch: "amd64"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, result1.Checksum, result2.Checksum)
|
||||
|
|
@ -87,10 +89,10 @@ func TestChecksum_Good(t *testing.T) {
|
|||
path1 := setupChecksumTestFile(t, content)
|
||||
path2 := setupChecksumTestFile(t, content)
|
||||
|
||||
result1, err := Checksum(Artifact{Path: path1, OS: "linux", Arch: "amd64"})
|
||||
result1, err := Checksum(fs, Artifact{Path: path1, OS: "linux", Arch: "amd64"})
|
||||
require.NoError(t, err)
|
||||
|
||||
result2, err := Checksum(Artifact{Path: path2, OS: "linux", Arch: "amd64"})
|
||||
result2, err := Checksum(fs, Artifact{Path: path2, OS: "linux", Arch: "amd64"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, result1.Checksum, result2.Checksum)
|
||||
|
|
@ -98,6 +100,7 @@ func TestChecksum_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChecksum_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns error for empty path", func(t *testing.T) {
|
||||
artifact := Artifact{
|
||||
Path: "",
|
||||
|
|
@ -105,7 +108,7 @@ func TestChecksum_Bad(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := Checksum(artifact)
|
||||
result, err := Checksum(fs, artifact)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "artifact path is empty")
|
||||
assert.Empty(t, result.Checksum)
|
||||
|
|
@ -118,7 +121,7 @@ func TestChecksum_Bad(t *testing.T) {
|
|||
Arch: "amd64",
|
||||
}
|
||||
|
||||
result, err := Checksum(artifact)
|
||||
result, err := Checksum(fs, artifact)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to open file")
|
||||
assert.Empty(t, result.Checksum)
|
||||
|
|
@ -126,6 +129,7 @@ func TestChecksum_Bad(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChecksumAll_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("checksums multiple artifacts", func(t *testing.T) {
|
||||
paths := []string{
|
||||
setupChecksumTestFile(t, "content one"),
|
||||
|
|
@ -139,7 +143,7 @@ func TestChecksumAll_Good(t *testing.T) {
|
|||
{Path: paths[2], OS: "windows", Arch: "amd64"},
|
||||
}
|
||||
|
||||
results, err := ChecksumAll(artifacts)
|
||||
results, err := ChecksumAll(fs, artifacts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 3)
|
||||
|
||||
|
|
@ -152,19 +156,20 @@ func TestChecksumAll_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("returns nil for empty slice", func(t *testing.T) {
|
||||
results, err := ChecksumAll([]Artifact{})
|
||||
results, err := ChecksumAll(fs, []Artifact{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, results)
|
||||
})
|
||||
|
||||
t.Run("returns nil for nil slice", func(t *testing.T) {
|
||||
results, err := ChecksumAll(nil)
|
||||
results, err := ChecksumAll(fs, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, results)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChecksumAll_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns partial results on error", func(t *testing.T) {
|
||||
path := setupChecksumTestFile(t, "valid content")
|
||||
|
||||
|
|
@ -173,7 +178,7 @@ func TestChecksumAll_Bad(t *testing.T) {
|
|||
{Path: "/nonexistent/file", OS: "linux", Arch: "arm64"}, // This will fail
|
||||
}
|
||||
|
||||
results, err := ChecksumAll(artifacts)
|
||||
results, err := ChecksumAll(fs, artifacts)
|
||||
assert.Error(t, err)
|
||||
// Should have the first successful result
|
||||
assert.Len(t, results, 1)
|
||||
|
|
@ -182,6 +187,7 @@ func TestChecksumAll_Bad(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWriteChecksumFile_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("writes checksum file with correct format", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "CHECKSUMS.txt")
|
||||
|
|
@ -191,7 +197,7 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
{Path: "/output/app_darwin_arm64.tar.gz", Checksum: "789xyz000111", OS: "darwin", Arch: "arm64"},
|
||||
}
|
||||
|
||||
err := WriteChecksumFile(artifacts, checksumPath)
|
||||
err := WriteChecksumFile(fs, artifacts, checksumPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read and verify content
|
||||
|
|
@ -214,7 +220,7 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
{Path: "/output/app.tar.gz", Checksum: "abc123", OS: "linux", Arch: "amd64"},
|
||||
}
|
||||
|
||||
err := WriteChecksumFile(artifacts, checksumPath)
|
||||
err := WriteChecksumFile(fs, artifacts, checksumPath)
|
||||
require.NoError(t, err)
|
||||
assert.FileExists(t, checksumPath)
|
||||
})
|
||||
|
|
@ -223,7 +229,7 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "CHECKSUMS.txt")
|
||||
|
||||
err := WriteChecksumFile([]Artifact{}, checksumPath)
|
||||
err := WriteChecksumFile(fs, []Artifact{}, checksumPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// File should not exist
|
||||
|
|
@ -235,7 +241,7 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "CHECKSUMS.txt")
|
||||
|
||||
err := WriteChecksumFile(nil, checksumPath)
|
||||
err := WriteChecksumFile(fs, nil, checksumPath)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
|
|
@ -247,7 +253,7 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
{Path: "/some/deep/nested/path/myapp_linux_amd64.tar.gz", Checksum: "checksum123", OS: "linux", Arch: "amd64"},
|
||||
}
|
||||
|
||||
err := WriteChecksumFile(artifacts, checksumPath)
|
||||
err := WriteChecksumFile(fs, artifacts, checksumPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(checksumPath)
|
||||
|
|
@ -260,6 +266,7 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWriteChecksumFile_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns error for artifact without checksum", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "CHECKSUMS.txt")
|
||||
|
|
@ -268,7 +275,7 @@ func TestWriteChecksumFile_Bad(t *testing.T) {
|
|||
{Path: "/output/app.tar.gz", Checksum: "", OS: "linux", Arch: "amd64"}, // No checksum
|
||||
}
|
||||
|
||||
err := WriteChecksumFile(artifacts, checksumPath)
|
||||
err := WriteChecksumFile(fs, artifacts, checksumPath)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "has no checksum")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/host-uk/core/pkg/build/signing"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -68,10 +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)
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
content, err := fs.Read(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return DefaultConfig(), nil
|
||||
|
|
@ -80,6 +81,7 @@ func LoadConfig(dir string) (*BuildConfig, error) {
|
|||
}
|
||||
|
||||
var cfg BuildConfig
|
||||
data := []byte(content)
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("build.LoadConfig: failed to parse config file: %w", err)
|
||||
}
|
||||
|
|
@ -153,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.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -28,6 +29,7 @@ func setupConfigTestDir(t *testing.T, configContent string) string {
|
|||
}
|
||||
|
||||
func TestLoadConfig_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("loads valid config", func(t *testing.T) {
|
||||
content := `
|
||||
version: 1
|
||||
|
|
@ -54,7 +56,7 @@ targets:
|
|||
`
|
||||
dir := setupConfigTestDir(t, content)
|
||||
|
||||
cfg, err := LoadConfig(dir)
|
||||
cfg, err := LoadConfig(fs, dir)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
|
|
@ -77,7 +79,7 @@ targets:
|
|||
t.Run("returns defaults when config file missing", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
cfg, err := LoadConfig(dir)
|
||||
cfg, err := LoadConfig(fs, dir)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
|
|
@ -98,7 +100,7 @@ project:
|
|||
`
|
||||
dir := setupConfigTestDir(t, content)
|
||||
|
||||
cfg, err := LoadConfig(dir)
|
||||
cfg, err := LoadConfig(fs, dir)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
|
|
@ -128,7 +130,7 @@ targets:
|
|||
`
|
||||
dir := setupConfigTestDir(t, content)
|
||||
|
||||
cfg, err := LoadConfig(dir)
|
||||
cfg, err := LoadConfig(fs, dir)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
|
|
@ -141,6 +143,7 @@ targets:
|
|||
}
|
||||
|
||||
func TestLoadConfig_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns error for invalid YAML", func(t *testing.T) {
|
||||
content := `
|
||||
version: 1
|
||||
|
|
@ -149,7 +152,7 @@ project:
|
|||
`
|
||||
dir := setupConfigTestDir(t, content)
|
||||
|
||||
cfg, err := LoadConfig(dir)
|
||||
cfg, err := LoadConfig(fs, dir)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, cfg)
|
||||
assert.Contains(t, err.Error(), "failed to parse config file")
|
||||
|
|
@ -166,7 +169,7 @@ project:
|
|||
err = os.Mkdir(configPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := LoadConfig(dir)
|
||||
cfg, err := LoadConfig(fs, dir)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, cfg)
|
||||
assert.Contains(t, err.Error(), "failed to read config file")
|
||||
|
|
@ -217,19 +220,20 @@ func TestConfigPath_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConfigExists_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns true when config exists", func(t *testing.T) {
|
||||
dir := setupConfigTestDir(t, "version: 1")
|
||||
assert.True(t, ConfigExists(dir))
|
||||
assert.True(t, ConfigExists(fs, dir))
|
||||
})
|
||||
|
||||
t.Run("returns false when config missing", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
assert.False(t, ConfigExists(dir))
|
||||
assert.False(t, ConfigExists(fs, dir))
|
||||
})
|
||||
|
||||
t.Run("returns false when .core dir missing", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
assert.False(t, ConfigExists(dir))
|
||||
assert.False(t, ConfigExists(fs, dir))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +253,7 @@ sign:
|
|||
`
|
||||
_ = os.WriteFile(filepath.Join(coreDir, "build.yaml"), []byte(configContent), 0644)
|
||||
|
||||
cfg, err := LoadConfig(tmpDir)
|
||||
cfg, err := LoadConfig(io.Local, tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -298,8 +302,9 @@ func TestBuildConfig_ToTargets_Good(t *testing.T) {
|
|||
|
||||
// TestLoadConfig_Testdata tests loading from the testdata fixture.
|
||||
func TestLoadConfig_Testdata(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("loads config-project fixture", func(t *testing.T) {
|
||||
cfg, err := LoadConfig("testdata/config-project")
|
||||
cfg, err := LoadConfig(fs, "testdata/config-project")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// Marker files for project type detection.
|
||||
|
|
@ -32,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)
|
||||
|
|
@ -50,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
|
||||
}
|
||||
|
|
@ -62,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 {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
func fileExists(fs io.Medium, path string) bool {
|
||||
return fs.IsFile(path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -22,52 +23,54 @@ func setupTestDir(t *testing.T, markers ...string) string {
|
|||
}
|
||||
|
||||
func TestDiscover_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("detects Go project", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "go.mod")
|
||||
types, err := Discover(dir)
|
||||
types, err := Discover(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []ProjectType{ProjectTypeGo}, types)
|
||||
})
|
||||
|
||||
t.Run("detects Wails project with priority over Go", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "wails.json", "go.mod")
|
||||
types, err := Discover(dir)
|
||||
types, err := Discover(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []ProjectType{ProjectTypeWails, ProjectTypeGo}, types)
|
||||
})
|
||||
|
||||
t.Run("detects Node.js project", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "package.json")
|
||||
types, err := Discover(dir)
|
||||
types, err := Discover(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []ProjectType{ProjectTypeNode}, types)
|
||||
})
|
||||
|
||||
t.Run("detects PHP project", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "composer.json")
|
||||
types, err := Discover(dir)
|
||||
types, err := Discover(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []ProjectType{ProjectTypePHP}, types)
|
||||
})
|
||||
|
||||
t.Run("detects multiple project types", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "go.mod", "package.json")
|
||||
types, err := Discover(dir)
|
||||
types, err := Discover(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []ProjectType{ProjectTypeGo, ProjectTypeNode}, types)
|
||||
})
|
||||
|
||||
t.Run("empty directory returns empty slice", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
types, err := Discover(dir)
|
||||
types, err := Discover(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, types)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDiscover_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("non-existent directory returns empty slice", func(t *testing.T) {
|
||||
types, err := Discover("/non/existent/path")
|
||||
types, err := Discover(fs, "/non/existent/path")
|
||||
assert.NoError(t, err) // os.Stat fails silently in fileExists
|
||||
assert.Empty(t, types)
|
||||
})
|
||||
|
|
@ -78,85 +81,90 @@ func TestDiscover_Bad(t *testing.T) {
|
|||
err := os.Mkdir(filepath.Join(dir, "go.mod"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
types, err := Discover(dir)
|
||||
types, err := Discover(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, types)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrimaryType_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns wails for wails project", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "wails.json", "go.mod")
|
||||
primary, err := PrimaryType(dir)
|
||||
primary, err := PrimaryType(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ProjectTypeWails, primary)
|
||||
})
|
||||
|
||||
t.Run("returns go for go-only project", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "go.mod")
|
||||
primary, err := PrimaryType(dir)
|
||||
primary, err := PrimaryType(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ProjectTypeGo, primary)
|
||||
})
|
||||
|
||||
t.Run("returns empty string for empty directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
primary, err := PrimaryType(dir)
|
||||
primary, err := PrimaryType(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, primary)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsGoProject_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("true with go.mod", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "go.mod")
|
||||
assert.True(t, IsGoProject(dir))
|
||||
assert.True(t, IsGoProject(fs, dir))
|
||||
})
|
||||
|
||||
t.Run("true with wails.json", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "wails.json")
|
||||
assert.True(t, IsGoProject(dir))
|
||||
assert.True(t, IsGoProject(fs, dir))
|
||||
})
|
||||
|
||||
t.Run("false without markers", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
assert.False(t, IsGoProject(dir))
|
||||
assert.False(t, IsGoProject(fs, dir))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsWailsProject_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("true with wails.json", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "wails.json")
|
||||
assert.True(t, IsWailsProject(dir))
|
||||
assert.True(t, IsWailsProject(fs, dir))
|
||||
})
|
||||
|
||||
t.Run("false with only go.mod", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "go.mod")
|
||||
assert.False(t, IsWailsProject(dir))
|
||||
assert.False(t, IsWailsProject(fs, dir))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsNodeProject_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("true with package.json", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "package.json")
|
||||
assert.True(t, IsNodeProject(dir))
|
||||
assert.True(t, IsNodeProject(fs, dir))
|
||||
})
|
||||
|
||||
t.Run("false without package.json", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
assert.False(t, IsNodeProject(dir))
|
||||
assert.False(t, IsNodeProject(fs, dir))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsPHPProject_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("true with composer.json", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "composer.json")
|
||||
assert.True(t, IsPHPProject(dir))
|
||||
assert.True(t, IsPHPProject(fs, dir))
|
||||
})
|
||||
|
||||
t.Run("false without composer.json", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
assert.False(t, IsPHPProject(dir))
|
||||
assert.False(t, IsPHPProject(fs, dir))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -166,27 +174,29 @@ func TestTarget_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFileExists_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns true for existing file", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.txt")
|
||||
err := os.WriteFile(path, []byte("content"), 0644)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, fileExists(path))
|
||||
assert.True(t, fileExists(fs, path))
|
||||
})
|
||||
|
||||
t.Run("returns false for directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
assert.False(t, fileExists(dir))
|
||||
assert.False(t, fileExists(fs, dir))
|
||||
})
|
||||
|
||||
t.Run("returns false for non-existent path", func(t *testing.T) {
|
||||
assert.False(t, fileExists("/non/existent/file"))
|
||||
assert.False(t, fileExists(fs, "/non/existent/file"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestDiscover_Testdata tests discovery using the testdata fixtures.
|
||||
// These serve as integration tests with realistic project structures.
|
||||
func TestDiscover_Testdata(t *testing.T) {
|
||||
fs := io.Local
|
||||
testdataDir := "testdata"
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -205,7 +215,7 @@ func TestDiscover_Testdata(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dir := filepath.Join(testdataDir, tt.dir)
|
||||
types, err := Discover(dir)
|
||||
types, err := Discover(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
if len(tt.expected) == 0 {
|
||||
assert.Empty(t, types)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ package signing
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// MacOSSigner signs binaries using macOS codesign.
|
||||
|
|
@ -39,7 +40,7 @@ func (s *MacOSSigner) Available() bool {
|
|||
}
|
||||
|
||||
// Sign codesigns a binary with hardened runtime.
|
||||
func (s *MacOSSigner) Sign(ctx context.Context, binary string) error {
|
||||
func (s *MacOSSigner) Sign(ctx context.Context, fs io.Medium, binary string) error {
|
||||
if !s.Available() {
|
||||
return fmt.Errorf("codesign.Sign: codesign not available")
|
||||
}
|
||||
|
|
@ -62,7 +63,7 @@ func (s *MacOSSigner) Sign(ctx context.Context, binary string) error {
|
|||
|
||||
// Notarize submits binary to Apple for notarization and staples the ticket.
|
||||
// This blocks until Apple responds (typically 1-5 minutes).
|
||||
func (s *MacOSSigner) Notarize(ctx context.Context, binary string) error {
|
||||
func (s *MacOSSigner) Notarize(ctx context.Context, fs io.Medium, binary string) error {
|
||||
if s.config.AppleID == "" || s.config.TeamID == "" || s.config.AppPassword == "" {
|
||||
return fmt.Errorf("codesign.Notarize: missing Apple credentials (apple_id, team_id, app_password)")
|
||||
}
|
||||
|
|
@ -73,7 +74,7 @@ func (s *MacOSSigner) Notarize(ctx context.Context, binary string) error {
|
|||
if output, err := zipCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("codesign.Notarize: failed to create zip: %w\nOutput: %s", err, string(output))
|
||||
}
|
||||
defer func() { _ = os.Remove(zipPath) }()
|
||||
defer func() { _ = fs.Delete(zipPath) }()
|
||||
|
||||
// Submit to Apple and wait
|
||||
submitCmd := exec.CommandContext(ctx, "xcrun", "notarytool", "submit",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -34,17 +35,19 @@ func TestMacOSSigner_Sign_Bad(t *testing.T) {
|
|||
if runtime.GOOS == "darwin" {
|
||||
t.Skip("skipping on macOS")
|
||||
}
|
||||
fs := io.Local
|
||||
s := NewMacOSSigner(MacOSConfig{Identity: "test"})
|
||||
err := s.Sign(context.Background(), "test")
|
||||
err := s.Sign(context.Background(), fs, "test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not available")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMacOSSigner_Notarize_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("fails with missing credentials", func(t *testing.T) {
|
||||
s := NewMacOSSigner(MacOSConfig{})
|
||||
err := s.Notarize(context.Background(), "test")
|
||||
err := s.Notarize(context.Background(), fs, "test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "missing Apple credentials")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// GPGSigner signs files using GPG.
|
||||
|
|
@ -35,7 +37,7 @@ func (s *GPGSigner) Available() bool {
|
|||
|
||||
// Sign creates a detached ASCII-armored signature.
|
||||
// For file.txt, creates file.txt.asc
|
||||
func (s *GPGSigner) Sign(ctx context.Context, file string) error {
|
||||
func (s *GPGSigner) Sign(ctx context.Context, fs io.Medium, file string) error {
|
||||
if !s.Available() {
|
||||
return fmt.Errorf("gpg.Sign: gpg not available or key not configured")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -23,9 +24,10 @@ func TestGPGSigner_Bad_NoKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGPGSigner_Sign_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("fails when no key", func(t *testing.T) {
|
||||
s := NewGPGSigner("")
|
||||
err := s.Sign(context.Background(), "test.txt")
|
||||
err := s.Sign(context.Background(), fs, "test.txt")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not available or key not configured")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// Artifact represents a build output that can be signed.
|
||||
|
|
@ -16,7 +18,7 @@ type Artifact struct {
|
|||
|
||||
// SignBinaries signs macOS binaries in the artifacts list.
|
||||
// Only signs darwin binaries when running on macOS with a configured identity.
|
||||
func SignBinaries(ctx context.Context, cfg SignConfig, artifacts []Artifact) error {
|
||||
func SignBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifacts []Artifact) error {
|
||||
if !cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -37,7 +39,7 @@ func SignBinaries(ctx context.Context, cfg SignConfig, artifacts []Artifact) err
|
|||
}
|
||||
|
||||
fmt.Printf(" Signing %s...\n", artifact.Path)
|
||||
if err := signer.Sign(ctx, artifact.Path); err != nil {
|
||||
if err := signer.Sign(ctx, fs, artifact.Path); err != nil {
|
||||
return fmt.Errorf("failed to sign %s: %w", artifact.Path, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +48,7 @@ func SignBinaries(ctx context.Context, cfg SignConfig, artifacts []Artifact) err
|
|||
}
|
||||
|
||||
// NotarizeBinaries notarizes macOS binaries if enabled.
|
||||
func NotarizeBinaries(ctx context.Context, cfg SignConfig, artifacts []Artifact) error {
|
||||
func NotarizeBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifacts []Artifact) error {
|
||||
if !cfg.Enabled || !cfg.MacOS.Notarize {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -66,7 +68,7 @@ func NotarizeBinaries(ctx context.Context, cfg SignConfig, artifacts []Artifact)
|
|||
}
|
||||
|
||||
fmt.Printf(" Notarizing %s (this may take a few minutes)...\n", artifact.Path)
|
||||
if err := signer.Notarize(ctx, artifact.Path); err != nil {
|
||||
if err := signer.Notarize(ctx, fs, artifact.Path); err != nil {
|
||||
return fmt.Errorf("failed to notarize %s: %w", artifact.Path, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +77,7 @@ func NotarizeBinaries(ctx context.Context, cfg SignConfig, artifacts []Artifact)
|
|||
}
|
||||
|
||||
// SignChecksums signs the checksums file with GPG.
|
||||
func SignChecksums(ctx context.Context, cfg SignConfig, checksumFile string) error {
|
||||
func SignChecksums(ctx context.Context, fs io.Medium, cfg SignConfig, checksumFile string) error {
|
||||
if !cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -86,7 +88,7 @@ func SignChecksums(ctx context.Context, cfg SignConfig, checksumFile string) err
|
|||
}
|
||||
|
||||
fmt.Printf(" Signing %s with GPG...\n", checksumFile)
|
||||
if err := signer.Sign(ctx, checksumFile); err != nil {
|
||||
if err := signer.Sign(ctx, fs, checksumFile); err != nil {
|
||||
return fmt.Errorf("failed to sign checksums: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// Signer defines the interface for code signing implementations.
|
||||
|
|
@ -14,7 +16,7 @@ type Signer interface {
|
|||
// Available checks if this signer can be used.
|
||||
Available() bool
|
||||
// Sign signs the artifact at the given path.
|
||||
Sign(ctx context.Context, path string) error
|
||||
Sign(ctx context.Context, fs io.Medium, path string) error
|
||||
}
|
||||
|
||||
// SignConfig holds signing configuration from .core/build.yaml.
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import (
|
|||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSignBinaries_Good_SkipsNonDarwin(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
Enabled: true,
|
||||
MacOS: MacOSConfig{
|
||||
|
|
@ -23,7 +25,7 @@ func TestSignBinaries_Good_SkipsNonDarwin(t *testing.T) {
|
|||
}
|
||||
|
||||
// Should not error even though binary doesn't exist (skips non-darwin)
|
||||
err := SignBinaries(ctx, cfg, artifacts)
|
||||
err := SignBinaries(ctx, fs, cfg, artifacts)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -31,6 +33,7 @@ func TestSignBinaries_Good_SkipsNonDarwin(t *testing.T) {
|
|||
|
||||
func TestSignBinaries_Good_DisabledConfig(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
|
|
@ -39,7 +42,7 @@ func TestSignBinaries_Good_DisabledConfig(t *testing.T) {
|
|||
{Path: "/tmp/test-binary", OS: "darwin", Arch: "arm64"},
|
||||
}
|
||||
|
||||
err := SignBinaries(ctx, cfg, artifacts)
|
||||
err := SignBinaries(ctx, fs, cfg, artifacts)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -51,6 +54,7 @@ func TestSignBinaries_Good_SkipsOnNonMacOS(t *testing.T) {
|
|||
}
|
||||
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
Enabled: true,
|
||||
MacOS: MacOSConfig{
|
||||
|
|
@ -62,7 +66,7 @@ func TestSignBinaries_Good_SkipsOnNonMacOS(t *testing.T) {
|
|||
{Path: "/tmp/test-binary", OS: "darwin", Arch: "arm64"},
|
||||
}
|
||||
|
||||
err := SignBinaries(ctx, cfg, artifacts)
|
||||
err := SignBinaries(ctx, fs, cfg, artifacts)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -70,6 +74,7 @@ func TestSignBinaries_Good_SkipsOnNonMacOS(t *testing.T) {
|
|||
|
||||
func TestNotarizeBinaries_Good_DisabledConfig(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
|
|
@ -78,7 +83,7 @@ func TestNotarizeBinaries_Good_DisabledConfig(t *testing.T) {
|
|||
{Path: "/tmp/test-binary", OS: "darwin", Arch: "arm64"},
|
||||
}
|
||||
|
||||
err := NotarizeBinaries(ctx, cfg, artifacts)
|
||||
err := NotarizeBinaries(ctx, fs, cfg, artifacts)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -86,6 +91,7 @@ func TestNotarizeBinaries_Good_DisabledConfig(t *testing.T) {
|
|||
|
||||
func TestNotarizeBinaries_Good_NotarizeDisabled(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
Enabled: true,
|
||||
MacOS: MacOSConfig{
|
||||
|
|
@ -97,7 +103,7 @@ func TestNotarizeBinaries_Good_NotarizeDisabled(t *testing.T) {
|
|||
{Path: "/tmp/test-binary", OS: "darwin", Arch: "arm64"},
|
||||
}
|
||||
|
||||
err := NotarizeBinaries(ctx, cfg, artifacts)
|
||||
err := NotarizeBinaries(ctx, fs, cfg, artifacts)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -105,6 +111,7 @@ func TestNotarizeBinaries_Good_NotarizeDisabled(t *testing.T) {
|
|||
|
||||
func TestSignChecksums_Good_SkipsNoKey(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
Enabled: true,
|
||||
GPG: GPGConfig{
|
||||
|
|
@ -113,7 +120,7 @@ func TestSignChecksums_Good_SkipsNoKey(t *testing.T) {
|
|||
}
|
||||
|
||||
// Should silently skip when no key
|
||||
err := SignChecksums(ctx, cfg, "/tmp/CHECKSUMS.txt")
|
||||
err := SignChecksums(ctx, fs, cfg, "/tmp/CHECKSUMS.txt")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -121,11 +128,12 @@ func TestSignChecksums_Good_SkipsNoKey(t *testing.T) {
|
|||
|
||||
func TestSignChecksums_Good_Disabled(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
err := SignChecksums(ctx, cfg, "/tmp/CHECKSUMS.txt")
|
||||
err := SignChecksums(ctx, fs, cfg, "/tmp/CHECKSUMS.txt")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -146,8 +154,9 @@ func TestSignConfig_ExpandEnv(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWindowsSigner_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
s := NewWindowsSigner(WindowsConfig{})
|
||||
assert.Equal(t, "signtool", s.Name())
|
||||
assert.False(t, s.Available())
|
||||
assert.NoError(t, s.Sign(context.Background(), "test.exe"))
|
||||
assert.NoError(t, s.Sign(context.Background(), fs, "test.exe"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package signing
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// WindowsSigner signs binaries using Windows signtool (placeholder).
|
||||
|
|
@ -28,7 +30,7 @@ func (s *WindowsSigner) Available() bool {
|
|||
}
|
||||
|
||||
// Sign is a placeholder that does nothing.
|
||||
func (s *WindowsSigner) Sign(ctx context.Context, binary string) error {
|
||||
func (s *WindowsSigner) Sign(ctx context.Context, fs io.Medium, binary string) error {
|
||||
// TODO: Implement Windows signing
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
71
pkg/io/io.go
71
pkg/io/io.go
|
|
@ -1,6 +1,7 @@
|
|||
package io
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -48,6 +49,12 @@ type Medium interface {
|
|||
// Stat returns file information for the given path.
|
||||
Stat(path string) (fs.FileInfo, error)
|
||||
|
||||
// Open opens the named file for reading.
|
||||
Open(path string) (fs.File, error)
|
||||
|
||||
// Create creates or truncates the named file.
|
||||
Create(path string) (io.WriteCloser, error)
|
||||
|
||||
// Exists checks if a path exists (file or directory).
|
||||
Exists(path string) bool
|
||||
|
||||
|
|
@ -307,6 +314,70 @@ func (m *MockMedium) Rename(oldPath, newPath string) error {
|
|||
return coreerr.E("io.MockMedium.Rename", "path not found: "+oldPath, os.ErrNotExist)
|
||||
}
|
||||
|
||||
// Open opens a file from the mock filesystem.
|
||||
func (m *MockMedium) Open(path string) (fs.File, error) {
|
||||
content, ok := m.Files[path]
|
||||
if !ok {
|
||||
return nil, coreerr.E("io.MockMedium.Open", "file not found: "+path, os.ErrNotExist)
|
||||
}
|
||||
return &MockFile{
|
||||
name: filepath.Base(path),
|
||||
content: []byte(content),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create creates a file in the mock filesystem.
|
||||
func (m *MockMedium) Create(path string) (io.WriteCloser, error) {
|
||||
return &MockWriteCloser{
|
||||
medium: m,
|
||||
path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MockFile implements fs.File for MockMedium.
|
||||
type MockFile struct {
|
||||
name string
|
||||
content []byte
|
||||
offset int64
|
||||
}
|
||||
|
||||
func (f *MockFile) Stat() (fs.FileInfo, error) {
|
||||
return FileInfo{
|
||||
name: f.name,
|
||||
size: int64(len(f.content)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *MockFile) Read(b []byte) (int, error) {
|
||||
if f.offset >= int64(len(f.content)) {
|
||||
return 0, fs.ErrClosed // Or io.EOF?
|
||||
}
|
||||
n := copy(b, f.content[f.offset:])
|
||||
f.offset += int64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (f *MockFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MockWriteCloser implements WriteCloser for MockMedium.
|
||||
type MockWriteCloser struct {
|
||||
medium *MockMedium
|
||||
path string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (w *MockWriteCloser) Write(p []byte) (int, error) {
|
||||
w.data = append(w.data, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *MockWriteCloser) Close() error {
|
||||
w.medium.Files[w.path] = string(w.data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns directory entries for the mock filesystem.
|
||||
func (m *MockMedium) List(path string) ([]fs.DirEntry, error) {
|
||||
if _, ok := m.Dirs[path]; !ok {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -106,6 +107,20 @@ func (m *Medium) Stat(p string) (fs.FileInfo, error) {
|
|||
return os.Stat(m.path(p))
|
||||
}
|
||||
|
||||
// Open opens the named file for reading.
|
||||
func (m *Medium) Open(p string) (fs.File, error) {
|
||||
return os.Open(m.path(p))
|
||||
}
|
||||
|
||||
// Create creates or truncates the named file.
|
||||
func (m *Medium) Create(p string) (io.WriteCloser, error) {
|
||||
full := m.path(p)
|
||||
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Create(full)
|
||||
}
|
||||
|
||||
// Delete removes a file or empty directory.
|
||||
func (m *Medium) Delete(p string) error {
|
||||
full := m.path(p)
|
||||
|
|
|
|||
|
|
@ -208,8 +208,11 @@ 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) {
|
||||
// Use local filesystem as the default medium
|
||||
fs := io.Local
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
@ -248,7 +251,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)
|
||||
}
|
||||
|
|
@ -260,6 +263,7 @@ func buildArtifacts(ctx context.Context, cfg *Config, projectDir, version string
|
|||
|
||||
// Build configuration
|
||||
buildConfig := &build.Config{
|
||||
FS: fs,
|
||||
ProjectDir: projectDir,
|
||||
OutputDir: outputDir,
|
||||
Name: binaryName,
|
||||
|
|
@ -274,20 +278,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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue