Files
releasekit/internal/artifact/artifact.go

120 lines
3.5 KiB
Go

// Package artifact owns shared build artifact identity rules.
package artifact
import (
"fmt"
"path/filepath"
"strings"
)
const ManifestFilename = "SHA256SUMS"
// OutputDirectory resolves build output and prebuilt asset directories from
// the absolute selected module directory.
func OutputDirectory(moduleDir, directory string) string {
if directory == "" {
directory = "dist"
}
if !filepath.IsAbs(directory) {
directory = filepath.Join(moduleDir, directory)
}
return filepath.Clean(directory)
}
type Target struct {
OS string
Arch string
}
// Settings contains normalized inputs shared by build execution and evidence.
type Settings struct {
MainPackage string
BinaryName string
Targets []Target
}
type Declaration struct {
Target Target
Filename string
}
// NormalizeSettings copies build inputs and removes duplicate targets while
// preserving their first occurrence.
func NormalizeSettings(mainPackage, binaryName string, targets []Target) Settings {
return Settings{
MainPackage: mainPackage,
BinaryName: binaryName,
Targets: NormalizeTargets(targets),
}
}
// NormalizeTargets removes duplicate targets while preserving their first
// occurrence.
func NormalizeTargets(targets []Target) []Target {
seen := make(map[Target]struct{}, len(targets))
result := make([]Target, 0, len(targets))
for _, target := range targets {
if _, ok := seen[target]; ok {
continue
}
seen[target] = struct{}{}
result = append(result, target)
}
return result
}
// ValidComponent reports whether value is a nonempty filename component under
// the build and receipt contracts.
func ValidComponent(value string) bool {
return value != "" && !strings.ContainsAny(value, "/\\\r\n")
}
// Filename returns the declared binary filename for one target.
func Filename(binaryName, version string, target Target) string {
filename := fmt.Sprintf("%s-%s-%s-%s", binaryName, version, target.OS, target.Arch)
if target.OS == "windows" {
filename += ".exe"
}
return filename
}
// ExpectedBuild returns artifact declarations in target order. Callers retain
// responsibility for validating their boundary-specific build identity.
func ExpectedBuild(binaryName, version string, targets []Target) []Declaration {
declarations := make([]Declaration, 0, len(targets))
for _, target := range targets {
declarations = append(declarations, Declaration{
Target: target,
Filename: Filename(binaryName, version, target),
})
}
return declarations
}
// ValidateDeclarations matches recorded artifact identities to selected targets.
func ValidateDeclarations(binaryName, version string, targets []Target, recorded []Declaration) error {
if len(recorded) != len(targets) {
return fmt.Errorf("artifact and target counts do not match")
}
want := make(map[Target]string, len(targets))
for _, declaration := range ExpectedBuild(binaryName, version, targets) {
target := declaration.Target
if !ValidComponent(target.OS) || !ValidComponent(target.Arch) {
return fmt.Errorf("invalid target %q/%q", target.OS, target.Arch)
}
if _, exists := want[target]; exists {
return fmt.Errorf("repeats target %q/%q", target.OS, target.Arch)
}
want[target] = declaration.Filename
}
seen := make(map[Target]bool, len(recorded))
for _, declaration := range recorded {
name, ok := want[declaration.Target]
if !ok || seen[declaration.Target] || declaration.Filename != name {
return fmt.Errorf("artifact %q does not match a selected target and its declared name", declaration.Filename)
}
seen[declaration.Target] = true
}
return nil
}