120 lines
3.5 KiB
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
|
|
}
|