// 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 }