Prepare recovery tools for the host target

This commit is contained in:
2026-11-04 00:50:59 +00:00
parent 10028c4abb
commit d8ec9389e0
3 changed files with 53 additions and 12 deletions

View File

@@ -57,8 +57,8 @@ func TestVendoredAPIOnlyPublicationRecovery(t *testing.T) {
if err != nil {
t.Fatal(err)
}
launcher := exec.Command(goBinary, append(launcherArgs, "publish", "--repo", repo, "v1.2.3")...)
launcher.Dir = t.TempDir()
launcher := exec.Command(goBinary, append(launcherArgs.Args, "publish", "--repo", repo, "v1.2.3")...)
launcher.Dir = launcherArgs.Directory
publicationOutput, err := launcher.CombinedOutput()
var exitError *exec.ExitError
if !errors.As(err, &exitError) || exitError.ExitCode() != 1 {
@@ -98,12 +98,29 @@ func TestRecoveryToolPreservesVersionedReplacement(t *testing.T) {
if err := os.WriteFile(filepath.Join(module, "go.mod"), []byte(data), 0600); err != nil {
t.Fatal(err)
}
shimDir := t.TempDir()
logPath := filepath.Join(t.TempDir(), "target")
realGo := filepath.Join(runtime.GOROOT(), "bin", "go")
shim := "#!/bin/sh\ncase \" $* \" in *\" -deps \"*) printf '%s/%s\\n' \"$GOOS\" \"$GOARCH\" > \"$RECOVERY_TARGET_LOG\" ;; esac\nexec \"$RECOVERY_REAL_GO\" \"$@\"\n"
if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(shim), 0700); err != nil {
t.Fatal(err)
}
t.Setenv("RECOVERY_TARGET_LOG", logPath)
t.Setenv("RECOVERY_REAL_GO", realGo)
t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH"))
foreignOS, foreignArch := foreignTarget()
t.Setenv("GOOS", foreignOS)
t.Setenv("GOARCH", foreignArch)
args, err := gotool.RecoveryTool(t.Context(), module, t.TempDir())
if err != nil {
t.Fatal(err)
}
command := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...)
command.Dir = t.TempDir()
if target, err := os.ReadFile(logPath); err != nil || strings.TrimSpace(string(target)) != runtime.GOOS+"/"+runtime.GOARCH {
t.Fatalf("recovery preparation target = %q, %v", target, err)
}
command := exec.Command(realGo, args.Args...)
command.Dir = args.Directory
command.Env = setEnvironment(setEnvironment(os.Environ(), "GOOS", runtime.GOOS), "GOARCH", runtime.GOARCH)
output, err := command.CombinedOutput()
if err != nil || !strings.Contains(string(output), "tool-version=v0.2.198 helper=tool-only dependency") {
t.Fatalf("versioned recovery selection: %v: %s", err, output)
@@ -119,8 +136,8 @@ func TestRecoveryToolSupportsReleaseKitSourceModule(t *testing.T) {
if err != nil {
t.Fatal(err)
}
command := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), append(args, "version")...)
command.Dir = t.TempDir()
command := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), append(args.Args, "version")...)
command.Dir = args.Directory
output, err := command.CombinedOutput()
if err != nil || !strings.HasPrefix(string(output), "releasekit ") {
t.Fatalf("source module recovery entry point: %v: %s", err, output)

View File

@@ -96,6 +96,8 @@ checks in Stage 6.
## Stage 4: Normalize module-based recovery preparation
**Completed.** The CLI adapter and assembled handoff tests follow in the next stage.
1. In [the Go recovery implementation](../../internal/gotool/recovery.go),
keep the existing registered-tool, API-only, and no-dependency selections.
Have `RecoveryTool` return `(*RecoveryInvocation, error)` within the

View File

@@ -43,10 +43,16 @@ func TemporaryExecutable(path string) bool {
return err == nil
}
// RecoveryTool returns module-selected Go arguments for a later invocation.
// An empty result means this module does not select ReleaseKit. API-only modules
// RecoveryInvocation describes a module-selected Go tool invocation.
type RecoveryInvocation struct {
Directory string
Args []string
}
// RecoveryTool returns a module-selected Go invocation for later use.
// A nil result means this module does not select ReleaseKit. API-only modules
// receive a private module root that outlives launcher-owned bootstraps.
func RecoveryTool(ctx context.Context, moduleDir, recordDir string) ([]string, error) {
func RecoveryTool(ctx context.Context, moduleDir, recordDir string) (*RecoveryInvocation, error) {
path := filepath.Join(moduleDir, "go.mod")
data, err := os.ReadFile(path)
if err != nil {
@@ -58,7 +64,7 @@ func RecoveryTool(ctx context.Context, moduleDir, recordDir string) ([]string, e
}
for _, tool := range module.Tool {
if tool.Path == releaseKitCommand {
return []string{"-C", moduleDir, "tool", "-modfile=", releaseKitCommand}, nil
return &RecoveryInvocation{Directory: moduleDir, Args: []string{"tool", "-modfile=", releaseKitCommand}}, nil
}
}
// Ask Go about the selected module graph, including transitive dependencies.
@@ -140,15 +146,31 @@ func RecoveryTool(ctx context.Context, moduleDir, recordDir string) ([]string, e
return nil, err
}
// Prepare the private graph and sums before the later tool invocation.
environment := map[string]string{"GOWORK": "off", "GOTOOLCHAIN": "local"}
hostJSON, err := discover(ctx, directory, environment, "go", "env", "-json", "GOHOSTOS", "GOHOSTARCH")
if err != nil {
return nil, fmt.Errorf("prepare recovery tool: %w", err)
}
var host struct {
GOHOSTOS string
GOHOSTARCH string
}
if err := json.Unmarshal(hostJSON, &host); err != nil {
return nil, fmt.Errorf("prepare recovery tool: decode host target: %w", err)
}
if host.GOHOSTOS == "" || host.GOHOSTARCH == "" {
return nil, fmt.Errorf("prepare recovery tool: missing host target")
}
environment["GOOS"], environment["GOARCH"] = host.GOHOSTOS, host.GOHOSTARCH
captured := &combinedWriter{mode: captureStatus}
err = process.Run(ctx, process.Invocation{
Args: []string{"go", "list", "-mod=mod", "-modfile=", "-deps", releaseKitCommand},
Dir: directory, Env: map[string]string{"GOWORK": "off", "GOTOOLCHAIN": "local"},
Dir: directory, Env: environment,
Stdout: captured, Stderr: captured,
})
if err != nil {
return nil, fmt.Errorf("prepare recovery tool: %w: %s", err, diagnosticText(captured.take(), captured.omitted()))
}
complete = true
return []string{"-C", directory, "tool", "-modfile=", releaseKitCommand}, nil
return &RecoveryInvocation{Directory: directory, Args: []string{"tool", "-modfile=", releaseKitCommand}}, nil
}