Compile packages for selected targets

This commit is contained in:
2026-10-07 22:27:31 +00:00
parent 1c41f7a0d1
commit cabc56b134
20 changed files with 406 additions and 40 deletions

View File

@@ -0,0 +1,204 @@
package validation
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestRunCompilesTargetsAfterTestAndVetInSelectionOrder(t *testing.T) {
logPath := installGoShim(t, "", false)
targets := []Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, {OS: "darwin", Arch: "arm64"}}
err := Run(context.Background(), Request{
RepositoryDir: t.TempDir(),
Settings: Settings{FullVet: true, CompileTargets: targets},
})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
var commands []string
for _, command := range readGoShimLog(t, logPath) {
if strings.HasPrefix(command, "test ") || strings.HasPrefix(command, "vet ") || strings.HasPrefix(command, "build ") {
commands = append(commands, command)
}
}
if len(commands) != len(targets)+2 || !strings.HasPrefix(commands[0], "test ") || !strings.HasPrefix(commands[1], "vet ") {
t.Fatalf("validation commands = %q, want test, vet, then one build per target", commands)
}
for index, target := range targets {
command := commands[index+2]
if !strings.HasPrefix(command, "build ") {
t.Fatalf("command %d = %q, want package build", index+2, command)
}
for _, argument := range []string{"build", "-mod=readonly", "-o " + os.DevNull, "./..."} {
if !strings.Contains(" "+command+" ", " "+argument+" ") {
t.Errorf("build command %q does not contain %q", command, argument)
}
}
if got, want := strings.Fields(command)[0], "build"; got != want {
t.Errorf("command = %q, want %q", got, want)
}
if target.OS == "" || target.Arch == "" {
t.Fatalf("test target is incomplete: %#v", target)
}
}
environments, err := os.ReadFile(os.Getenv("RELEASEKIT_GO_ENV_LOG"))
if err != nil {
t.Fatal(err)
}
got := strings.Split(strings.TrimSpace(string(environments)), "\n")
if len(got) != len(targets) {
t.Fatalf("static build environments = %q, want %d entries", got, len(targets))
}
for index, target := range targets {
if want := fmt.Sprintf("%s/%s|0", target.OS, target.Arch); got[index] != want {
t.Errorf("static build environment %d = %q, want %q", index, got[index], want)
}
}
}
func TestRunAttributesTargetCompileFailureAndStops(t *testing.T) {
logPath := installGoShim(t, "darwin/arm64", false)
targets := []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}, {OS: "windows", Arch: "amd64"}}
err := Run(context.Background(), Request{
RepositoryDir: t.TempDir(),
Settings: Settings{FullVet: true, CompileTargets: targets},
})
var failure *Failure
if !errors.As(err, &failure) || failure.Check != PackageCompile {
t.Fatalf("Run() error = %v, want a package-compile failure", err)
}
if len(failure.Findings) != 1 || failure.Findings[0].Target != "darwin/arm64" {
t.Fatalf("package compilation findings = %#v, want target darwin/arm64", failure.Findings)
}
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
t.Fatalf("Run() error = %v, want the Go command failure preserved", err)
}
var commands []string
for _, command := range readGoShimLog(t, logPath) {
if strings.HasPrefix(command, "test ") || strings.HasPrefix(command, "vet ") || strings.HasPrefix(command, "build ") {
commands = append(commands, command)
}
}
if len(commands) != 4 || !strings.HasPrefix(commands[0], "test ") || !strings.HasPrefix(commands[1], "vet ") || !strings.HasPrefix(commands[2], "build ") || !strings.HasPrefix(commands[3], "build ") {
t.Fatalf("validation commands after target failure = %q, want test, vet, two builds", commands)
}
}
func TestRunRejectsIncompleteCompileTargets(t *testing.T) {
installGoShim(t, "", false)
for _, target := range []Target{{OS: "linux"}, {Arch: "amd64"}, {OS: " ", Arch: "amd64"}, {OS: "linux/other", Arch: "amd64"}} {
err := Run(context.Background(), Request{
RepositoryDir: t.TempDir(),
Settings: Settings{CompileTargets: []Target{target}},
})
var failure *Failure
if !errors.As(err, &failure) || failure.Check != PackageCompile {
t.Errorf("Run(%#v) error = %v, want a package-compile validation failure", target, err)
continue
}
if len(failure.Findings) != 1 || failure.Findings[0].Target != target.OS+"/"+target.Arch {
t.Errorf("Run(%#v) findings = %#v, want the incomplete target", target, failure.Findings)
}
}
}
func TestRunCompilesSourceOnlyAndExecutablePackagesWithoutArtifacts(t *testing.T) {
fixtures := []struct {
name string
files map[string]string
}{
{
name: "source library",
files: map[string]string{
"library.go": "package fixture\n\nconst Value = 1\n",
},
},
{
name: "single main",
files: map[string]string{
"library.go": "package fixture\n\nconst Value = 1\n",
"cmd/app/main.go": "package main\n\nfunc main() {}\n",
},
},
{
name: "multiple mains with repeated basenames",
files: map[string]string{
"library.go": "package fixture\n\nconst Value = 1\n",
"cmd/one/tool/main.go": "package main\n\nfunc main() {}\n",
"cmd/two/tool/main.go": "package main\n\nfunc main() {}\n",
},
},
}
for _, fixture := range fixtures {
t.Run(fixture.name, func(t *testing.T) {
module := writeCompileFixture(t, fixture.files)
before := readCompileFixture(t, module, fixture.files)
var targets []Target
if fixture.name == "source library" {
targets = []Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, {OS: "darwin", Arch: "arm64"}}
} else {
targets = []Target{{OS: "linux", Arch: "amd64"}}
}
if err := Run(context.Background(), Request{
RepositoryDir: module,
Settings: Settings{CompileTargets: targets},
}); err != nil {
t.Fatalf("Run() error = %v", err)
}
after := readCompileFixture(t, module, fixture.files)
for path, contents := range before {
if string(after[path]) != string(contents) {
t.Errorf("candidate file %q changed during compilation", path)
}
}
})
}
}
func writeCompileFixture(t *testing.T, files map[string]string) string {
t.Helper()
module := t.TempDir()
if err := os.WriteFile(filepath.Join(module, "go.mod"), []byte("module example.com/compilefixture\n\ngo 1.25.0\n"), 0o600); err != nil {
t.Fatal(err)
}
for path, contents := range files {
filename := filepath.Join(module, filepath.FromSlash(path))
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
return module
}
func readCompileFixture(t *testing.T, module string, files map[string]string) map[string][]byte {
t.Helper()
contents := make(map[string][]byte, len(files)+1)
for _, path := range append([]string{"go.mod"}, fixturePaths(files)...) {
data, err := os.ReadFile(filepath.Join(module, filepath.FromSlash(path)))
if err != nil {
t.Fatal(err)
}
contents[path] = data
}
return contents
}
func fixturePaths(files map[string]string) []string {
paths := make([]string, 0, len(files))
for path := range files {
paths = append(paths, path)
}
return paths
}

View File

@@ -13,6 +13,8 @@ const (
GoTest Check = "go-test"
// GoVet identifies the separate full vet pass.
GoVet Check = "go-vet"
// PackageCompile identifies selected-target package compilation.
PackageCompile Check = "package-compile"
)
// Finding describes a validation observation at an optional source location.

View File

@@ -213,6 +213,7 @@ func installGoShim(t *testing.T, failure string, delayVet bool) string {
}
directory := t.TempDir()
logPath := filepath.Join(directory, "go-commands")
envLogPath := filepath.Join(directory, "go-environment")
settings := map[string]string{
"GOHOSTOS": runtime.GOOS,
"GOHOSTARCH": runtime.GOARCH,
@@ -250,8 +251,15 @@ if [ "${1-}" = "vet" ] && [ "%s" = "vet" ]; then
echo "Go vet failed" >&2
exit 2
fi
if [ "${1-}" = "build" ]; then
printf '%%s/%%s|%%s\n' "${GOOS-}" "${GOARCH-}" "${CGO_ENABLED-}" >> "$RELEASEKIT_GO_ENV_LOG"
if [ "%s" = "build" ] || [ "%s" = "${GOOS-}/${GOARCH-}" ]; then
echo "Go build failed" >&2
exit 2
fi
fi
%s
`, failure, failure, failure, vetDelay)
`, failure, failure, failure, failure, failure, vetDelay)
script := fmt.Sprintf(`#!/bin/sh
set -eu
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
@@ -272,6 +280,7 @@ exit 0
}
t.Setenv("PATH", path)
t.Setenv("RELEASEKIT_GO_LOG", logPath)
t.Setenv("RELEASEKIT_GO_ENV_LOG", envLogPath)
return logPath
}

View File

@@ -3,15 +3,25 @@ package validation
import (
"context"
"fmt"
"io"
"os"
"strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
)
// Settings selects shared validation behavior.
type Settings struct {
Race bool
FullVet bool
Race bool
FullVet bool
CompileTargets []Target
}
// Target selects one package-compilation target.
type Target struct {
OS string
Arch string
}
// Request identifies the captured source and output used by shared validation.
@@ -30,6 +40,17 @@ func (request Request) ReadCandidateTree(ctx context.Context) (*CandidateTree, e
// Run executes shared validation for the captured candidate.
func Run(ctx context.Context, request Request) error {
for _, target := range request.Settings.CompileTargets {
if strings.TrimSpace(target.OS) == "" || strings.TrimSpace(target.Arch) == "" || strings.Contains(target.OS, "/") || strings.Contains(target.Arch, "/") {
return &Failure{
Check: PackageCompile,
Findings: []Finding{{
Target: target.OS + "/" + target.Arch,
Message: "target must be one complete GOOS/GOARCH pair",
}},
}
}
}
tool, err := gotool.New(ctx, request.RepositoryDir)
if err != nil {
return &Failure{Check: GoTest, Cause: err}
@@ -52,5 +73,20 @@ func Run(ctx context.Context, request Request) error {
return &Failure{Check: GoVet, Cause: err}
}
}
for _, target := range request.Settings.CompileTargets {
selected := gotool.Target{OS: target.OS, Arch: target.Arch}
_, err = tool.Run(ctx, request.RepositoryDir, gotool.Static, selected, request.Output,
"build", "-mod=readonly", "-o", os.DevNull, "./...")
if err != nil {
return &Failure{
Check: PackageCompile,
Findings: []Finding{{
Target: fmt.Sprintf("%s/%s", target.OS, target.Arch),
Message: "package compilation failed",
}},
Cause: err,
}
}
}
return nil
}