internal/relui: migrate Go installer construction off old builders

Move installer construction to happen in the same place that installer
signing already happens, under the ConstructInstallerOnly build types
that CL 550320 added.

Reuse the minimal xar parser that Russ implemented in cmd/gorebuild for
extracting signed binaries (that need to be inserted into the .tar.gz)
from a signed .pkg installer. This lets us drop the Xcode dependency
for that task and have it run as part of relui on secured machines
used only for release orchestration.

Delete the unused previous code and supporting files for building
installers, since what's used now is in the internal/installer
packages that CL 550321 added.

For golang/go#63147.

Change-Id: If8b207b7e3739052bc6d5f8ac13bbe5a05b50e0c
Cq-Include-Trybots: luci.golang.try:x_build-gotip-linux-amd64-longtest-race
Reviewed-on: https://go-review.googlesource.com/c/build/+/552016
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/gorebuild/darwin.go b/cmd/gorebuild/darwin.go
index 06f057d..4610634 100644
--- a/cmd/gorebuild/darwin.go
+++ b/cmd/gorebuild/darwin.go
@@ -175,9 +175,12 @@
 	return ix
 }
 
-// xar parser, enough to read macOS pkg files.
-// https://en.wikipedia.org/wiki/Xar_(archiver)
-// https://github.com/mackyle/xar/wiki/xarformat
+// A minimal xar parser, enough to read macOS .pkg files.
+// Package golang.org/x/build/internal/task also has one
+// for its internal needs.
+//
+// See https://en.wikipedia.org/wiki/Xar_(archiver)
+// and https://github.com/mackyle/xar/wiki/xarformat.
 
 // xarHeader is the main XML data structure for the xar header.
 type xarHeader struct {
@@ -215,8 +218,8 @@
 // pkgPayload parses data as a macOS pkg file for the Go installer
 // and returns the content of the file org.golang.go.pkg/Payload.
 func pkgPayload(log *Log, data []byte) []byte {
-	if string(data[0:4]) != "xar!" || len(data) < 28 {
-		log.Printf("not an xar! file")
+	if len(data) < 28 || string(data[0:4]) != "xar!" {
+		log.Printf("not an XAR file format (missing a 28+ byte header with 'xar!' magic number)")
 		return nil
 	}
 	be := binary.BigEndian
diff --git a/internal/relui/buildrelease_test.go b/internal/relui/buildrelease_test.go
index 9352e36..3f83fee 100644
--- a/internal/relui/buildrelease_test.go
+++ b/internal/relui/buildrelease_test.go
@@ -68,20 +68,6 @@
 const fakeGo = `#!/bin/bash -eu
 
 case "$1" in
-"run")
-  case "$2" in
-  "releaselet.go")
-    # We're building an MSI. The command should be run in the gomote work dir.
-    ls go/src/make.bash >/dev/null
-    mkdir msi
-    echo "I'm an MSI!" > msi/thisisanmsi.msi
-    ;;
-  *)
-    echo "unknown main file $2"
-    exit 1
-    ;;
-  esac
-  ;;
 "get")
   ls go.mod go.sum >/dev/null
   for i in "${@:2}"; do
@@ -128,48 +114,7 @@
 	// Set up a server that will be used to serve inputs to the build.
 	bootstrapServer := httptest.NewServer(http.HandlerFunc(serveBootstrap))
 	t.Cleanup(bootstrapServer.Close)
-	fakeBuildlets := task.NewFakeBuildlets(t, bootstrapServer.URL, map[string]string{
-		"pkgbuild": `#!/bin/bash -eu
-case "$@" in
-"--identifier=org.golang.go --version ` + wantVersion + ` --scripts=pkg-scripts --root=pkg-root pkg-intermediate/org.golang.go.pkg")
-	# We're doing an intermediate step in building a PKG.
-	echo "I'm an intermediate PKG!" > "$6"
-	tar -cz -C pkg-root . >> "$6"
-	;;
-*)
-	echo "unexpected command $@"
-	exit 1
-	;;
-esac
-`,
-		"productbuild": `#!/bin/bash -eu
-case "$@" in
-"--distribution=pkg-distribution --resources=pkg-resources --package-path=pkg-intermediate pkg-out/` + wantVersion + `.pkg")
-	# We're building a PKG.
-	ls pkg-distribution pkg-resources/bg-light.png pkg-resources/bg-dark.png >/dev/null
-	cat pkg-intermediate/* | head -n 1 | sed "s/an intermediate PKG/a PKG/" > "$4"
-	cat pkg-intermediate/* | tail -n +2 >> "$4"
-	;;
-*)
-	echo "unexpected command $@"
-	exit 1
-	;;
-esac
-`,
-		"pkgutil": `#!/bin/bash -eu
-case "$@" in
-"--expand-full go.pkg pkg-expanded")
-	# We're expanding a PKG.
-	mkdir -p "$3/org.golang.go.pkg/Payload/usr/local/go"
-	tail -n +2 "$2" | tar -xz -C "$3/org.golang.go.pkg/Payload"
-	;;
-*)
-	echo "unexpected command $@"
-	exit 1
-	;;
-esac
-`,
-	})
+	fakeBuildlets := task.NewFakeBuildlets(t, bootstrapServer.URL, nil)
 
 	// Set up the fake CDN publishing process.
 	servingDir := t.TempDir()
diff --git a/internal/relui/workflows.go b/internal/relui/workflows.go
index a322c6e..4b5df69 100644
--- a/internal/relui/workflows.go
+++ b/internal/relui/workflows.go
@@ -12,6 +12,7 @@
 	"compress/gzip"
 	"context"
 	"crypto/sha256"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -31,12 +32,15 @@
 	"golang.org/x/build/buildlet"
 	"golang.org/x/build/dashboard"
 	"golang.org/x/build/internal/gcsfs"
+	"golang.org/x/build/internal/installer/darwinpkg"
+	"golang.org/x/build/internal/installer/windowsmsi"
 	"golang.org/x/build/internal/releasetargets"
 	"golang.org/x/build/internal/relui/db"
 	"golang.org/x/build/internal/relui/sign"
 	"golang.org/x/build/internal/task"
 	"golang.org/x/build/internal/workflow"
 	wf "golang.org/x/build/internal/workflow"
+	"golang.org/x/exp/maps"
 	"golang.org/x/net/context/ctxhttp"
 	"google.golang.org/protobuf/types/known/structpb"
 )
@@ -548,12 +552,11 @@
 		// include the signed binaries.
 		switch target.GOOS {
 		case "darwin":
-			pkg := wf.Task2(wd, "Build PKG installer", tasks.buildDarwinPKG, version, tar)
+			pkg := wf.Task1(wd, "Build PKG installer", tasks.buildDarwinPKG, tar)
 			signedPKG := wf.Task2(wd, "Sign PKG installer", tasks.signArtifact, pkg, wf.Const(sign.BuildMacOS))
-			signedTGZ := wf.Task1(wd, "Convert PKG to .tgz", tasks.convertPKGToTGZ, signedPKG)
-			mergedTGZ := wf.Task2(wd, "Merge signed files into .tgz", tasks.mergeSignedToTGZ, tar, signedTGZ)
-			mod = wf.Task4(wd, "Merge signed files into module zip", tasks.mergeSignedToModule, version, timestamp, mod, signedTGZ)
-			artifacts = append(artifacts, signedPKG, mergedTGZ)
+			signedTGZ := wf.Task2(wd, "Merge signed files into .tgz", tasks.mergeSignedToTGZ, tar, signedPKG)
+			mod = wf.Task4(wd, "Merge signed files into module zip", tasks.mergeSignedToModule, version, timestamp, mod, signedPKG)
+			artifacts = append(artifacts, signedPKG, signedTGZ)
 		case "windows":
 			msi := wf.Task1(wd, "Build MSI installer", tasks.buildWindowsMSI, tar)
 			signedMSI := wf.Task2(wd, "Sign MSI installer", tasks.signArtifact, msi, wf.Const(sign.BuildWindows))
@@ -994,9 +997,11 @@
 
 func (b *BuildReleaseTasks) mergeSignedToTGZ(ctx *wf.TaskContext, unsigned, signed artifact) (artifact, error) {
 	return b.runBuildStep(ctx, unsigned.Target, nil, signed, "tar.gz", func(_ *task.BuildletStep, signed io.Reader, w io.Writer) error {
-		signedBinaries, err := loadBinaries(ctx, signed)
+		signedBinaries, err := task.ReadBinariesFromPKG(signed)
 		if err != nil {
 			return err
+		} else if _, ok := signedBinaries["go/bin/go"]; !ok {
+			return fmt.Errorf("didn't find go/bin/go among %d signed binaries %+q", len(signedBinaries), maps.Keys(signedBinaries))
 		}
 
 		// Copy files from the tgz, overwriting with binaries from the signed tar.
@@ -1051,9 +1056,11 @@
 
 func (b *BuildReleaseTasks) mergeSignedToModule(ctx *wf.TaskContext, version string, timestamp time.Time, mod moduleArtifact, signed artifact) (moduleArtifact, error) {
 	a, err := b.runBuildStep(ctx, nil, nil, signed, "signedmod.zip", func(_ *task.BuildletStep, signed io.Reader, w io.Writer) error {
-		signedBinaries, err := loadBinaries(ctx, signed)
+		signedBinaries, err := task.ReadBinariesFromPKG(signed)
 		if err != nil {
 			return err
+		} else if _, ok := signedBinaries["go/bin/go"]; !ok {
+			return fmt.Errorf("didn't find go/bin/go among %d signed binaries %+q", len(signedBinaries), maps.Keys(signedBinaries))
 		}
 
 		// Copy files from the module zip, overwriting with binaries from the signed tar.
@@ -1109,39 +1116,6 @@
 	return mod, nil
 }
 
-// loadBinaries reads binaries that we expect to have been signed by the
-// macOS signing process from tgz.
-func loadBinaries(ctx *wf.TaskContext, tgz io.Reader) (map[string][]byte, error) {
-	zr, err := gzip.NewReader(tgz)
-	if err != nil {
-		return nil, err
-	}
-	defer zr.Close()
-	tr := tar.NewReader(zr)
-
-	binaries := map[string][]byte{}
-	for {
-		th, err := tr.Next()
-		if err == io.EOF {
-			break
-		} else if err != nil {
-			return nil, err
-		}
-		if !strings.HasPrefix(th.Name, "go/bin/") && !strings.HasPrefix(th.Name, "go/pkg/tool/") {
-			continue
-		}
-		if th.Typeflag != tar.TypeReg || th.Mode&0100 == 0 {
-			continue
-		}
-		contents, err := io.ReadAll(tr)
-		if err != nil {
-			return nil, err
-		}
-		binaries[th.Name] = contents
-	}
-	return binaries, nil
-}
-
 func (b *BuildReleaseTasks) buildBinary(ctx *wf.TaskContext, target *releasetargets.Target, source artifact) (artifact, error) {
 	bc := dashboard.Builders[target.Builder]
 	return b.runBuildStep(ctx, target, bc, source, "tar.gz", func(bs *task.BuildletStep, r io.Reader, w io.Writer) error {
@@ -1149,23 +1123,72 @@
 	})
 }
 
-func (b *BuildReleaseTasks) buildDarwinPKG(ctx *wf.TaskContext, version string, binary artifact) (artifact, error) {
-	bc := dashboard.Builders[binary.Target.Builder]
-	return b.runBuildStep(ctx, binary.Target, bc, binary, "pkg", func(bs *task.BuildletStep, r io.Reader, w io.Writer) error {
-		return bs.BuildDarwinPKG(ctx, r, version, w)
-	})
-}
-func (b *BuildReleaseTasks) convertPKGToTGZ(ctx *wf.TaskContext, pkg artifact) (tgz artifact, _ error) {
-	bc := dashboard.Builders[pkg.Target.Builder]
-	return b.runBuildStep(ctx, pkg.Target, bc, pkg, "tar.gz", func(bs *task.BuildletStep, r io.Reader, w io.Writer) error {
-		return bs.ConvertPKGToTGZ(ctx, r, w)
+// buildDarwinPKG constructs an installer for the given binary artifact, to be signed.
+func (b *BuildReleaseTasks) buildDarwinPKG(ctx *wf.TaskContext, binary artifact) (artifact, error) {
+	return b.runBuildStep(ctx, binary.Target, nil, artifact{}, "pkg", func(_ *task.BuildletStep, _ io.Reader, w io.Writer) error {
+		metadataFile, err := jsonEncodeScratchFile(ctx, b.ScratchFS, darwinpkg.InstallerOptions{
+			GOARCH:          binary.Target.GOARCH,
+			MinMacOSVersion: binary.Target.MinMacOSVersion,
+		})
+		if err != nil {
+			return err
+		}
+		installerPaths, err := b.signArtifacts(ctx, sign.BuildMacOSConstructInstallerOnly, []string{
+			b.ScratchFS.URL(ctx, binary.Scratch),
+			b.ScratchFS.URL(ctx, metadataFile),
+		})
+		if err != nil {
+			return err
+		} else if len(installerPaths) != 1 {
+			return fmt.Errorf("got %d outputs, want 1 macOS .pkg installer", len(installerPaths))
+		} else if ext := path.Ext(installerPaths[0]); ext != ".pkg" {
+			return fmt.Errorf("got output extension %q, want .pkg", ext)
+		}
+		resultFS, err := gcsfs.FromURL(ctx, b.GCSClient, b.SignedURL)
+		if err != nil {
+			return err
+		}
+		r, err := resultFS.Open(installerPaths[0])
+		if err != nil {
+			return err
+		}
+		defer r.Close()
+		_, err = io.Copy(w, r)
+		return err
 	})
 }
 
+// buildWindowsMSI constructs an installer for the given binary artifact, to be signed.
 func (b *BuildReleaseTasks) buildWindowsMSI(ctx *wf.TaskContext, binary artifact) (artifact, error) {
-	bc := dashboard.Builders[binary.Target.Builder]
-	return b.runBuildStep(ctx, binary.Target, bc, binary, "msi", func(bs *task.BuildletStep, r io.Reader, w io.Writer) error {
-		return bs.BuildWindowsMSI(ctx, r, w)
+	return b.runBuildStep(ctx, binary.Target, nil, artifact{}, "msi", func(_ *task.BuildletStep, _ io.Reader, w io.Writer) error {
+		metadataFile, err := jsonEncodeScratchFile(ctx, b.ScratchFS, windowsmsi.InstallerOptions{
+			GOARCH: binary.Target.GOARCH,
+		})
+		if err != nil {
+			return err
+		}
+		installerPaths, err := b.signArtifacts(ctx, sign.BuildWindowsConstructInstallerOnly, []string{
+			b.ScratchFS.URL(ctx, binary.Scratch),
+			b.ScratchFS.URL(ctx, metadataFile),
+		})
+		if err != nil {
+			return err
+		} else if len(installerPaths) != 1 {
+			return fmt.Errorf("got %d outputs, want 1 Windows .msi installer", len(installerPaths))
+		} else if ext := path.Ext(installerPaths[0]); ext != ".msi" {
+			return fmt.Errorf("got output extension %q, want .msi", ext)
+		}
+		resultFS, err := gcsfs.FromURL(ctx, b.GCSClient, b.SignedURL)
+		if err != nil {
+			return err
+		}
+		r, err := resultFS.Open(installerPaths[0])
+		if err != nil {
+			return err
+		}
+		defer r.Close()
+		_, err = io.Copy(w, r)
+		return err
 	})
 }
 
@@ -1706,3 +1729,22 @@
 	})
 	return detail, err
 }
+
+// jsonEncodeScratchFile JSON encodes v into a new scratch file and returns its name.
+func jsonEncodeScratchFile(ctx *wf.TaskContext, fs *task.ScratchFS, v any) (name string, _ error) {
+	name, f, err := fs.OpenWrite(ctx, "f.json")
+	if err != nil {
+		return "", err
+	}
+	e := json.NewEncoder(f)
+	e.SetIndent("", "\t")
+	e.SetEscapeHTML(false)
+	if err := e.Encode(v); err != nil {
+		f.Close()
+		return "", err
+	}
+	if err := f.Close(); err != nil {
+		return "", err
+	}
+	return name, nil
+}
diff --git a/internal/task/_data/darwinpkg/blue-bg.png b/internal/task/_data/darwinpkg/blue-bg.png
deleted file mode 100644
index 9de0f09..0000000
--- a/internal/task/_data/darwinpkg/blue-bg.png
+++ /dev/null
Binary files differ
diff --git a/internal/task/_data/darwinpkg/brown-bg.png b/internal/task/_data/darwinpkg/brown-bg.png
deleted file mode 100644
index 9aec274..0000000
--- a/internal/task/_data/darwinpkg/brown-bg.png
+++ /dev/null
Binary files differ
diff --git a/internal/task/_data/darwinpkg/dist.xml b/internal/task/_data/darwinpkg/dist.xml
deleted file mode 100644
index 19752be..0000000
--- a/internal/task/_data/darwinpkg/dist.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8" standalone="no"?>
-<!--
- Copyright 2023 The Go Authors. All rights reserved.
- Use of this source code is governed by a BSD-style
- license that can be found in the LICENSE file.
--->
-
-<installer-gui-script minSpecVersion="1">
-  <title>Go</title>
-  <background mime-type="image/png" file="bg-light.png" alignment="left" />
-  <background-darkAqua mime-type="image/png" file="bg-dark.png" alignment="left" />
-  <options hostArchitectures="{{.HostArchs}}" customize="never" allow-external-scripts="no" />
-  <domains enable_localSystem="true" />
-  <installation-check script="installCheck();" />
-  <script>
-    function installCheck() {
-      if (!(system.compareVersions(system.version.ProductVersion, '{{.MinOS}}') >= 0)) {
-        my.result.title = 'Unable to install';
-        my.result.message = 'Go requires macOS {{.MinOS}} or later.';
-        my.result.type = 'Fatal';
-        return false;
-      }
-      if (system.files.fileExistsAtPath('/usr/local/go/bin/go')) {
-        my.result.title = 'Previous Installation Detected';
-        my.result.message = 'A previous installation of Go exists at /usr/local/go. This installer will remove the previous installation prior to installing. Please back up any data before proceeding.';
-        my.result.type = 'Warning';
-        return false;
-      }
-      return true;
-    }
-  </script>
-  <choices-outline>
-    <line choice="org.golang.go.choice" />
-  </choices-outline>
-  <choice id="org.golang.go.choice" title="Go">
-    <pkg-ref id="org.golang.go.pkg" />
-  </choice>
-  <pkg-ref id="org.golang.go.pkg" auth="Root">org.golang.go.pkg</pkg-ref>
-</installer-gui-script>
diff --git a/internal/task/buildrelease.go b/internal/task/buildrelease.go
index c50ab8a..8bb7e6e 100644
--- a/internal/task/buildrelease.go
+++ b/internal/task/buildrelease.go
@@ -7,16 +7,13 @@
 import (
 	"archive/tar"
 	"archive/zip"
-	"bytes"
 	"compress/gzip"
 	"context"
-	"embed"
 	"fmt"
 	"io"
 	"path"
 	"regexp"
 	"strings"
-	"text/template"
 	"time"
 
 	"golang.org/x/build/buildenv"
@@ -304,184 +301,6 @@
 	return makeEnv
 }
 
-// BuildDarwinPKG builds an unsigned macOS installer
-// for the given Go version and binary archive.
-// It writes the result to pkg.
-func (b *BuildletStep) BuildDarwinPKG(ctx *workflow.TaskContext, binaryArchive io.Reader, goVersion string, pkg io.Writer) error {
-	ctx.Printf("Building inner .pkg with pkgbuild.")
-	if err := b.exec(ctx, "mkdir", []string{"pkg-intermediate"}, buildlet.ExecOpts{SystemLevel: true}); err != nil {
-		return err
-	}
-	if err := b.Buildlet.PutTar(ctx, binaryArchive, "pkg-root/usr/local"); err != nil {
-		return err
-	}
-	if err := b.Buildlet.Put(ctx, strings.NewReader("/usr/local/go/bin\n"), "pkg-root/etc/paths.d/go", 0644); err != nil {
-		return err
-	}
-	if err := b.Buildlet.Put(ctx, strings.NewReader(`#!/bin/bash
-
-GOROOT=/usr/local/go
-echo "Removing previous installation"
-if [ -d $GOROOT ]; then
-	rm -r $GOROOT
-fi
-`), "pkg-scripts/preinstall", 0755); err != nil {
-		return err
-	}
-	if err := b.Buildlet.Put(ctx, strings.NewReader(`#!/bin/bash
-
-GOROOT=/usr/local/go
-echo "Fixing permissions"
-cd $GOROOT
-find . -exec chmod ugo+r \{\} \;
-find bin -exec chmod ugo+rx \{\} \;
-find . -type d -exec chmod ugo+rx \{\} \;
-chmod o-w .
-`), "pkg-scripts/postinstall", 0755); err != nil {
-		return err
-	}
-	if err := b.exec(ctx, "pkgbuild", []string{
-		"--identifier=org.golang.go",
-		"--version", goVersion,
-		"--scripts=pkg-scripts",
-		"--root=pkg-root",
-		"pkg-intermediate/org.golang.go.pkg",
-	}, buildlet.ExecOpts{SystemLevel: true}); err != nil {
-		return err
-	}
-
-	ctx.Printf("Building outer .pkg with productbuild.")
-	if err := b.exec(ctx, "mkdir", []string{"pkg-out"}, buildlet.ExecOpts{SystemLevel: true}); err != nil {
-		return err
-	}
-	bg, err := darwinPKGBackground(b.Target.GOARCH)
-	if err != nil {
-		return err
-	}
-	if err := b.Buildlet.Put(ctx, bytes.NewReader(bg), "pkg-resources/bg-light.png", 0644); err != nil {
-		return err
-	}
-	if err := b.Buildlet.Put(ctx, bytes.NewReader(bg), "pkg-resources/bg-dark.png", 0644); err != nil {
-		return err
-	}
-	var buf bytes.Buffer
-	distData := darwinDistData{
-		HostArchs: map[string]string{"amd64": "x86_64", "arm64": "arm64"}[b.Target.GOARCH],
-		MinOS:     b.Target.MinMacOSVersion,
-	}
-	if err := darwinDistTmpl.ExecuteTemplate(&buf, "dist.xml", distData); err != nil {
-		return err
-	}
-	if err := b.Buildlet.Put(ctx, &buf, "pkg-distribution", 0644); err != nil {
-		return err
-	}
-	if err := b.exec(ctx, "productbuild", []string{
-		"--distribution=pkg-distribution",
-		"--resources=pkg-resources",
-		"--package-path=pkg-intermediate",
-		"pkg-out/" + goVersion + ".pkg",
-	}, buildlet.ExecOpts{SystemLevel: true}); err != nil {
-		return err
-	}
-
-	return fetchFile(ctx, b.Buildlet, pkg, "pkg-out")
-}
-
-//go:embed _data/darwinpkg
-var darwinPKGData embed.FS
-
-func darwinPKGBackground(goarch string) ([]byte, error) {
-	switch goarch {
-	case "arm64":
-		return darwinPKGData.ReadFile("_data/darwinpkg/blue-bg.png")
-	case "amd64":
-		return darwinPKGData.ReadFile("_data/darwinpkg/brown-bg.png")
-	default:
-		return nil, fmt.Errorf("no background for GOARCH %q", goarch)
-	}
-}
-
-var darwinDistTmpl = template.Must(template.New("").ParseFS(darwinPKGData, "_data/darwinpkg/dist.xml"))
-
-type darwinDistData struct {
-	HostArchs string // hostArchitectures option value.
-	MinOS     string // Minimum required system.version.ProductVersion.
-}
-
-// ConvertPKGToTGZ converts a macOS installer (.pkg) to a .tar.gz tarball.
-func (b *BuildletStep) ConvertPKGToTGZ(ctx *workflow.TaskContext, in io.Reader, out io.Writer) error {
-	if err := b.Buildlet.Put(ctx, in, "go.pkg", 0400); err != nil {
-		return err
-	}
-
-	ctx.Printf("Expanding PKG installer payload.")
-	if err := b.exec(ctx, "pkgutil", []string{"--expand-full", "go.pkg", "pkg-expanded"}, buildlet.ExecOpts{SystemLevel: true}); err != nil {
-		return err
-	}
-
-	ctx.Printf("Compressing into a tarball.")
-	input, err := b.Buildlet.GetTar(ctx, "pkg-expanded/org.golang.go.pkg/Payload/usr/local/go")
-	if err != nil {
-		return err
-	}
-	defer input.Close()
-
-	gzReader, err := gzip.NewReader(input)
-	if err != nil {
-		return err
-	}
-	defer gzReader.Close()
-	reader := tar.NewReader(gzReader)
-	gzWriter := gzip.NewWriter(out)
-	writer := tar.NewWriter(gzWriter)
-	if err := adjustTar(reader, writer, "go/", nil); err != nil {
-		return err
-	}
-	if err := writer.Close(); err != nil {
-		return err
-	}
-	return gzWriter.Close()
-}
-
-//go:embed releaselet/releaselet.go
-var releaselet string
-
-func (b *BuildletStep) BuildWindowsMSI(ctx *workflow.TaskContext, binaryArchive io.Reader, msi io.Writer) error {
-	ctx.Printf("Installing bootstrap go")
-	if err := b.Buildlet.PutTarFromURL(ctx, b.BuildConfig.GoBootstrapURL(buildenv.Production), "bootstrap_go"); err != nil {
-		return err
-	}
-	if err := b.Buildlet.PutTar(ctx, binaryArchive, ""); err != nil {
-		return err
-	}
-	ctx.Printf("Pushing and running releaselet.")
-	if err := b.Buildlet.Put(ctx, strings.NewReader(releaselet), "releaselet.go", 0666); err != nil {
-		return err
-	}
-	if err := b.exec(ctx, "bootstrap_go/bin/go", []string{"run", "releaselet.go"}, buildlet.ExecOpts{
-		Dir: ".", // root of buildlet work directory
-	}); err != nil {
-		ctx.Printf("releaselet failed: %v", err)
-		b.Buildlet.ListDir(ctx, ".", buildlet.ListDirOpts{Recursive: true}, func(ent buildlet.DirEntry) {
-			ctx.Printf("remote: %v", ent)
-		})
-		return err
-	}
-	return fetchFile(ctx, b.Buildlet, msi, "msi")
-}
-
-// fetchFile fetches the specified directory from the given buildlet, and
-// writes the first file it finds in that directory to dest.
-func fetchFile(ctx *workflow.TaskContext, client buildlet.RemoteClient, dest io.Writer, dir string) error {
-	ctx.Printf("Downloading file from %q.", dir)
-	tgz, err := client.GetTar(context.Background(), dir)
-	if err != nil {
-		return err
-	}
-	defer tgz.Close()
-	return ExtractFile(tgz, dest, "*")
-}
-
 // ExtractFile copies the first file in tgz matching glob to dest.
 func ExtractFile(tgz io.Reader, dest io.Writer, glob string) error {
 	zr, err := gzip.NewReader(tgz)
diff --git a/internal/task/darwin.go b/internal/task/darwin.go
new file mode 100644
index 0000000..f3b7315
--- /dev/null
+++ b/internal/task/darwin.go
@@ -0,0 +1,264 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package task
+
+import (
+	"bufio"
+	"bytes"
+	"compress/gzip"
+	"compress/zlib"
+	"encoding/binary"
+	"encoding/xml"
+	"errors"
+	"fmt"
+	"io"
+	"io/fs"
+	"strconv"
+	"strings"
+)
+
+// ReadBinariesFromPKG reads pkg, the Go installer .pkg file, and returns
+// binaries in bin and pkg/tool directories within GOROOT which we expect
+// to have been signed by the macOS signing process.
+//
+// The map key is a relative path starting with "go/", like "go/bin/gofmt"
+// or "go/pkg/tool/darwin_arm64/test2json". The map value holds its bytes.
+func ReadBinariesFromPKG(pkg io.Reader) (map[string][]byte, error) {
+	// Reading the whole file into memory isn't ideal, but it makes
+	// the implementation of pkgPayload easier, and we only have at
+	// most a few .pkg installers to process.
+	data, err := io.ReadAll(pkg)
+	if err != nil {
+		return nil, err
+	}
+	payload, err := pkgPayload(data)
+	if errors.Is(err, errNoXARHeader) && bytes.HasPrefix(data, []byte("I'm a PKG! -signed <macOS>\n")) {
+		// This invalid XAR file is a fake installer produced by release tests.
+		// Since its prefix indicates it was signed, return a fake signed go command binary.
+		return map[string][]byte{"go/bin/go": []byte("fake go command -signed <macOS>")}, nil
+	} else if err != nil {
+		return nil, err
+	}
+	ix, err := indexCpioGz(payload)
+	if err != nil {
+		return nil, err
+	}
+	var binaries = make(map[string][]byte) // Relative path starting with "go/" → binary data.
+	for nameWithinPayload, f := range ix {
+		name, ok := strings.CutPrefix(nameWithinPayload, "./usr/local/") // Trim ./usr/local/go/ down to just go/.
+		if !ok {
+			continue
+		}
+		if !strings.HasPrefix(name, "go/bin/") && !strings.HasPrefix(name, "go/pkg/tool/") {
+			continue
+		}
+		if !f.Mode.IsRegular() || f.Mode.Perm()&0100 == 0 {
+			continue
+		}
+		binaries[name] = f.Data
+	}
+	return binaries, nil
+}
+
+// A minimal xar parser, enough to read macOS .pkg files.
+// Command golang.org/x/build/cmd/gorebuild also has one
+// for its internal needs.
+//
+// See https://en.wikipedia.org/wiki/Xar_(archiver)
+// and https://github.com/mackyle/xar/wiki/xarformat.
+
+// xarHeader is the main XML data structure for the xar header.
+type xarHeader struct {
+	XMLName xml.Name `xml:"xar"`
+	TOC     xarTOC   `xml:"toc"`
+}
+
+// xarTOC is the table of contents.
+type xarTOC struct {
+	Files []*xarFile `xml:"file"`
+}
+
+// xarFile is a single file in the table of contents.
+// Directories have Type "directory" and contain other files.
+type xarFile struct {
+	Data  xarFileData `xml:"data"`
+	Name  string      `xml:"name"`
+	Type  string      `xml:"type"` // "file", "directory"
+	Files []*xarFile  `xml:"file"`
+}
+
+// xarFileData is the metadata describing a single file.
+type xarFileData struct {
+	Length   int64       `xml:"length"`
+	Offset   int64       `xml:"offset"`
+	Size     int64       `xml:"size"`
+	Encoding xarEncoding `xml:"encoding"`
+}
+
+// xarEncoding has an attribute giving the encoding for a file's content.
+type xarEncoding struct {
+	Style string `xml:"style,attr"`
+}
+
+var errNoXARHeader = fmt.Errorf("not an XAR file format (missing a 28+ byte header with 'xar!' magic number)")
+
+// pkgPayload parses data as a macOS pkg file for the Go installer
+// and returns the content of the file org.golang.go.pkg/Payload.
+func pkgPayload(data []byte) ([]byte, error) {
+	if len(data) < 28 || string(data[0:4]) != "xar!" {
+		return nil, errNoXARHeader
+	}
+	be := binary.BigEndian
+	hdrSize := be.Uint16(data[4:])
+	vers := be.Uint16(data[6:])
+	tocCSize := be.Uint64(data[8:])
+	tocUSize := be.Uint64(data[16:])
+
+	if vers != 1 {
+		return nil, fmt.Errorf("bad xar version %d", vers)
+	}
+	if int(hdrSize) >= len(data) || uint64(len(data))-uint64(hdrSize) < tocCSize {
+		return nil, fmt.Errorf("xar header bounds not in file")
+	}
+
+	data = data[hdrSize:]
+	chdr, data := data[:tocCSize], data[tocCSize:]
+
+	// Header is zlib-compressed XML.
+	zr, err := zlib.NewReader(bytes.NewReader(chdr))
+	if err != nil {
+		return nil, fmt.Errorf("reading xar header: %v", err)
+	}
+	defer zr.Close()
+	hdrXML := make([]byte, tocUSize+1)
+	n, err := io.ReadFull(zr, hdrXML)
+	if uint64(n) != tocUSize {
+		return nil, fmt.Errorf("invalid xar header size %d", n)
+	}
+	if err != io.ErrUnexpectedEOF {
+		return nil, fmt.Errorf("reading xar header: %v", err)
+	}
+	hdrXML = hdrXML[:tocUSize]
+	var hdr xarHeader
+	if err := xml.Unmarshal(hdrXML, &hdr); err != nil {
+		return nil, fmt.Errorf("unmarshaling xar header: %v", err)
+	}
+
+	// Walk TOC file tree to find org.golang.go.pkg/Payload.
+	for _, f := range hdr.TOC.Files {
+		if f.Name == "org.golang.go.pkg" && f.Type == "directory" {
+			for _, f := range f.Files {
+				if f.Name == "Payload" {
+					if f.Type != "file" {
+						return nil, fmt.Errorf("bad xar payload type %s", f.Type)
+					}
+					if f.Data.Encoding.Style != "application/octet-stream" {
+						return nil, fmt.Errorf("bad xar encoding %s", f.Data.Encoding.Style)
+					}
+					if f.Data.Offset >= int64(len(data)) || f.Data.Size > int64(len(data))-f.Data.Offset {
+						return nil, fmt.Errorf("xar payload bounds not in file")
+					}
+					return data[f.Data.Offset:][:f.Data.Size], nil
+				}
+			}
+		}
+	}
+	return nil, fmt.Errorf("payload not found")
+}
+
+// A cpioFile represents a single file in a CPIO archive.
+type cpioFile struct {
+	Name string
+	Mode fs.FileMode
+	Data []byte
+}
+
+// indexCpioGz parses data as a gzip-compressed cpio file and returns an index of its content.
+func indexCpioGz(data []byte) (map[string]*cpioFile, error) {
+	zr, err := gzip.NewReader(bytes.NewReader(data))
+	if err != nil {
+		return nil, err
+	}
+	br := bufio.NewReader(zr)
+
+	const hdrSize = 76
+
+	ix := make(map[string]*cpioFile)
+	hdr := make([]byte, hdrSize)
+	for {
+		_, err := io.ReadFull(br, hdr)
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			return nil, fmt.Errorf("reading archive: %v", err)
+		}
+
+		// https://www.mkssoftware.com/docs/man4/cpio.4.asp
+		//
+		//	hdr[0:6] "070707"
+		//	hdr[6:12] device number (all numbers '0'-padded octal)
+		//	hdr[12:18] inode number
+		//	hdr[18:24] mode
+		//	hdr[24:30] uid
+		//	hdr[30:36] gid
+		//	hdr[36:42] nlink
+		//	hdr[42:48] rdev
+		//	hdr[48:59] mtime
+		//	hdr[59:65] name length
+		//	hdr[65:76] file size
+
+		if !allOctal(hdr[:]) || string(hdr[:6]) != "070707" {
+			return nil, fmt.Errorf("reading archive: malformed entry")
+		}
+		mode, _ := strconv.ParseInt(string(hdr[18:24]), 8, 64)
+		nameLen, _ := strconv.ParseInt(string(hdr[59:65]), 8, 64)
+		size, _ := strconv.ParseInt(string(hdr[65:76]), 8, 64)
+		nameBuf := make([]byte, nameLen)
+		if _, err := io.ReadFull(br, nameBuf); err != nil {
+			return nil, fmt.Errorf("reading archive: %v", err)
+		}
+		if nameLen == 0 || nameBuf[nameLen-1] != 0 {
+			return nil, fmt.Errorf("reading archive: malformed entry")
+		}
+		name := string(nameBuf[:nameLen-1])
+
+		// The MKS cpio page says "TRAILER!!"
+		// but the Apple pkg files use "TRAILER!!!".
+		if name == "TRAILER!!!" {
+			break
+		}
+
+		fmode := fs.FileMode(mode & 0777)
+		if mode&040000 != 0 {
+			fmode |= fs.ModeDir
+		}
+
+		data, err := io.ReadAll(io.LimitReader(br, size))
+		if err != nil {
+			return nil, fmt.Errorf("reading archive: %v", err)
+		}
+		if size != int64(len(data)) {
+			return nil, fmt.Errorf("reading archive: short file")
+		}
+
+		if fmode&fs.ModeDir != 0 {
+			continue
+		}
+
+		ix[name] = &cpioFile{name, fmode, data}
+	}
+	return ix, nil
+}
+
+// allOctal reports whether x is entirely ASCII octal digits.
+func allOctal(x []byte) bool {
+	for _, b := range x {
+		if b < '0' || '7' < b {
+			return false
+		}
+	}
+	return true
+}
diff --git a/internal/task/darwin_test.go b/internal/task/darwin_test.go
new file mode 100644
index 0000000..eead250
--- /dev/null
+++ b/internal/task/darwin_test.go
@@ -0,0 +1,99 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package task_test
+
+import (
+	"flag"
+	"io/fs"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/build/internal/task"
+)
+
+var readPKGFlag = flag.String("read-pkg", "", "Path to a Go macOS .pkg installer to run TestReadBinariesFromPKG with.")
+
+func TestReadBinariesFromPKG(t *testing.T) {
+	if *readPKGFlag == "" {
+		t.Skip("skipping manual test since -read-pkg flag is not set")
+	}
+	if _, err := exec.LookPath("pkgutil"); err != nil {
+		// Since this is a manual test, we can afford to fail
+		// rather than skip if required dependencies are missing.
+		t.Fatal("required dependency pkgutil not found in PATH:", err)
+	}
+	if ext := filepath.Ext(*readPKGFlag); ext != ".pkg" {
+		t.Fatalf("got input file extension %q, want .pkg", ext)
+	}
+	f, err := os.Open(*readPKGFlag)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f.Close()
+
+	got, err := task.ReadBinariesFromPKG(f)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want, err := readBinariesFromPKGUsingXcode(t, *readPKGFlag)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Compare with reflect.DeepEqual first for speed;
+	// there's 100 MB or so of binary data to compare.
+	if !reflect.DeepEqual(want, got) {
+		t.Log("got files:")
+		for path := range got {
+			t.Log("\t" + path)
+		}
+		t.Log("want files:")
+		for path := range want {
+			t.Log("\t" + path)
+		}
+		t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got))
+	}
+}
+
+// readBinariesFromPKGUsingXcode implements the same functionality as
+// ReadBinariesFromPKG but uses Xcode's pkgutil as its implementation.
+func readBinariesFromPKGUsingXcode(t *testing.T, pkgPath string) (map[string][]byte, error) {
+	expanded := filepath.Join(t.TempDir(), "expanded")
+	out, err := exec.Command("pkgutil", "--expand-full", pkgPath, expanded).CombinedOutput()
+	if err != nil {
+		t.Fatalf("pkgutil failed: %v\noutput: %s", err, out)
+	}
+	var binaries = make(map[string][]byte) // Relative path starting with "go/" → binary data.
+	root := filepath.Join(expanded, "org.golang.go.pkg/Payload/usr/local")
+	err = filepath.Walk(root, func(path string, fi fs.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		name, err := filepath.Rel(root, path)
+		if err != nil {
+			return err
+		}
+		if !strings.HasPrefix(name, "go/bin/") && !strings.HasPrefix(name, "go/pkg/tool/") {
+			return nil
+		}
+		if !fi.Mode().IsRegular() || fi.Mode().Perm()&0100 == 0 {
+			return nil
+		}
+		b, err := os.ReadFile(path)
+		if err != nil {
+			return err
+		}
+		binaries[name] = b
+		return nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	return binaries, nil
+}
diff --git a/internal/task/fakes.go b/internal/task/fakes.go
index fd99583..4f505c0 100644
--- a/internal/task/fakes.go
+++ b/internal/task/fakes.go
@@ -10,6 +10,7 @@
 	"compress/gzip"
 	"context"
 	"crypto/sha256"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -24,6 +25,7 @@
 	"path/filepath"
 	"reflect"
 	"regexp"
+	"strconv"
 	"strings"
 	"sync"
 	"testing"
@@ -34,6 +36,8 @@
 	"golang.org/x/build/buildlet"
 	"golang.org/x/build/gerrit"
 	"golang.org/x/build/internal/gcsfs"
+	"golang.org/x/build/internal/installer/darwinpkg"
+	"golang.org/x/build/internal/installer/windowsmsi"
 	"golang.org/x/build/internal/relui/sign"
 	"golang.org/x/build/internal/untar"
 	wf "golang.org/x/build/internal/workflow"
@@ -603,6 +607,17 @@
 	jobID = uuid.NewString()
 	var out []string
 	switch bt {
+	case sign.BuildMacOSConstructInstallerOnly:
+		if len(in) != 2 {
+			return "", fmt.Errorf("got %d inputs, want 2", len(in))
+		}
+		out = []string{s.fakeConstructPKG(jobID, in[0], in[1], fmt.Sprintf("-installer <%s>", bt))}
+	case sign.BuildWindowsConstructInstallerOnly:
+		if len(in) != 2 {
+			return "", fmt.Errorf("got %d inputs, want 2", len(in))
+		}
+		out = []string{s.fakeConstructMSI(jobID, in[0], in[1], fmt.Sprintf("-installer <%s>", bt))}
+
 	case sign.BuildMacOS:
 		if len(in) != 1 {
 			return "", fmt.Errorf("got %d inputs, want 1", len(in))
@@ -644,12 +659,78 @@
 	return fmt.Errorf("intentional fake error")
 }
 
+func (s *FakeSignService) fakeConstructPKG(jobID, f, meta, msg string) string {
+	// Check installer metadata.
+	b, err := os.ReadFile(strings.TrimPrefix(meta, "file://"))
+	if err != nil {
+		panic(fmt.Errorf("fakeConstructPKG: os.ReadFile: %v", err))
+	}
+	var opt darwinpkg.InstallerOptions
+	if err := json.Unmarshal(b, &opt); err != nil {
+		panic(fmt.Errorf("fakeConstructPKG: json.Unmarshal: %v", err))
+	}
+	var errs []error
+	switch opt.GOARCH {
+	case "amd64", "arm64": // OK.
+	default:
+		errs = append(errs, fmt.Errorf("unexpected GOARCH value: %q", opt.GOARCH))
+	}
+	switch min, _ := strconv.Atoi(opt.MinMacOSVersion); {
+	case min >= 11: // macOS 11 or greater; OK.
+	case opt.MinMacOSVersion == "10.15": // OK.
+	case opt.MinMacOSVersion == "10.13": // OK. Go 1.20 has macOS 10.13 as its minimum.
+	default:
+		errs = append(errs, fmt.Errorf("unexpected MinMacOSVersion value: %q", opt.MinMacOSVersion))
+	}
+	if err := errors.Join(errs...); err != nil {
+		panic(fmt.Errorf("fakeConstructPKG: unexpected installer options %#v: %v", opt, err))
+	}
+
+	// Construct fake installer.
+	b, err = os.ReadFile(strings.TrimPrefix(f, "file://"))
+	if err != nil {
+		panic(fmt.Errorf("fakeConstructPKG: os.ReadFile: %v", err))
+	}
+	return s.writeOutput(jobID, path.Base(f)+".pkg", append([]byte("I'm a PKG!\n"), b...))
+}
+
+func (s *FakeSignService) fakeConstructMSI(jobID, f, meta, msg string) string {
+	// Check installer metadata.
+	b, err := os.ReadFile(strings.TrimPrefix(meta, "file://"))
+	if err != nil {
+		panic(fmt.Errorf("fakeConstructMSI: os.ReadFile: %v", err))
+	}
+	var opt windowsmsi.InstallerOptions
+	if err := json.Unmarshal(b, &opt); err != nil {
+		panic(fmt.Errorf("fakeConstructMSI: json.Unmarshal: %v", err))
+	}
+	var errs []error
+	switch opt.GOARCH {
+	case "386", "amd64", "arm", "arm64": // OK.
+	default:
+		errs = append(errs, fmt.Errorf("unexpected GOARCH value: %q", opt.GOARCH))
+	}
+	if err := errors.Join(errs...); err != nil {
+		panic(fmt.Errorf("fakeConstructMSI: unexpected installer options %#v: %v", opt, err))
+	}
+
+	// Construct fake installer.
+	_, err = os.ReadFile(strings.TrimPrefix(f, "file://"))
+	if err != nil {
+		panic(fmt.Errorf("fakeConstructMSI: os.ReadFile: %v", err))
+	}
+	return s.writeOutput(jobID, path.Base(f)+".msi", []byte("I'm an MSI!\n"))
+}
+
 func (s *FakeSignService) fakeSignPKG(jobID, f, msg string) string {
 	b, err := os.ReadFile(strings.TrimPrefix(f, "file://"))
 	if err != nil {
 		panic(fmt.Errorf("fakeSignPKG: os.ReadFile: %v", err))
 	}
-	b = bytes.TrimPrefix(b, []byte("I'm a PKG!\n"))
+	b, ok := bytes.CutPrefix(b, []byte("I'm a PKG!\n"))
+	if !ok {
+		panic(fmt.Errorf("fakeSignPKG: input doesn't look like a PKG to be signed"))
+	}
 	files, err := tgzToMap(bytes.NewReader(b))
 	if err != nil {
 		panic(fmt.Errorf("fakeSignPKG: tgzToMap: %v", err))
diff --git a/internal/task/releaselet/README.md b/internal/task/releaselet/README.md
deleted file mode 100644
index 4335ba2..0000000
--- a/internal/task/releaselet/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-<!-- Auto-generated by x/build/update-readmes.go -->
-
-[![Go Reference](https://pkg.go.dev/badge/golang.org/x/build/internal/task/releaselet.svg)](https://pkg.go.dev/golang.org/x/build/internal/task/releaselet)
-
-# golang.org/x/build/internal/task/releaselet
-
-Command releaselet does buildlet-side release construction tasks.
diff --git a/internal/task/releaselet/releaselet.go b/internal/task/releaselet/releaselet.go
deleted file mode 100644
index 7bc0421..0000000
--- a/internal/task/releaselet/releaselet.go
+++ /dev/null
@@ -1,594 +0,0 @@
-// Copyright 2015 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Command releaselet does buildlet-side release construction tasks.
-// It is intended to be executed on the buildlet preparing a release
-// using the version of Go to be released.
-package main
-
-import (
-	"archive/zip"
-	"bytes"
-	"crypto/sha256"
-	"errors"
-	"fmt"
-	"io"
-	"log"
-	"net/http"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"regexp"
-	"runtime"
-	"strconv"
-	"strings"
-)
-
-func main() {
-	if runtime.GOOS != "windows" {
-		log.Fatal("releaselet is only necessary on Windows")
-	}
-	if err := windowsMSI(); err != nil {
-		log.Fatal(err)
-	}
-}
-
-func environ() (cwd, version string, _ error) {
-	cwd, err := os.Getwd()
-	if err != nil {
-		return "", "", err
-	}
-	versionFile, err := os.ReadFile("go/VERSION")
-	if err != nil {
-		return "", "", err
-	}
-	version, _, _ = strings.Cut(string(versionFile), "\n")
-	return cwd, version, nil
-}
-
-func windowsMSI() error {
-	cwd, version, err := environ()
-	if err != nil {
-		return err
-	}
-
-	// Install Wix tools.
-	wix := filepath.Join(cwd, "wix")
-	defer os.RemoveAll(wix)
-	fmt.Fprintln(os.Stderr, "installing wix")
-	switch runtime.GOARCH {
-	default:
-		if err := installWix(wixRelease311, wix); err != nil {
-			return err
-		}
-	case "arm64":
-		if err := installWix(wixRelease314, wix); err != nil {
-			return err
-		}
-	}
-
-	// Write out windows data that is used by the packaging process.
-	win := filepath.Join(cwd, "windows")
-	defer os.RemoveAll(win)
-	if err := writeDataFiles(windowsData, win); err != nil {
-		return err
-	}
-
-	// Gather files.
-	fmt.Fprintln(os.Stderr, "running wix heat")
-	goDir := filepath.Join(cwd, "go")
-	appfiles := filepath.Join(win, "AppFiles.wxs")
-	if err := runDir(win, filepath.Join(wix, "heat"),
-		"dir", goDir,
-		"-nologo",
-		"-gg", "-g1", "-srd", "-sfrag", "-sreg",
-		"-cg", "AppFiles",
-		"-template", "fragment",
-		"-dr", "INSTALLDIR",
-		"-var", "var.SourceDir",
-		"-out", appfiles,
-	); err != nil {
-		return err
-	}
-
-	msArch := func() string {
-		switch runtime.GOARCH {
-		default:
-			panic("unknown arch for windows " + runtime.GOARCH)
-		case "386":
-			return "x86"
-		case "amd64":
-			return "x64"
-		case "arm64":
-			return "arm64"
-		}
-	}
-
-	// Build package.
-	verMajor, verMinor := splitVersion(version)
-
-	fmt.Fprintln(os.Stderr, "running wix candle")
-	if err := runDir(win, filepath.Join(wix, "candle"),
-		"-nologo",
-		"-arch", msArch(),
-		"-dGoVersion="+version,
-		fmt.Sprintf("-dGoMajorVersion=%v", verMajor),
-		fmt.Sprintf("-dWixGoVersion=1.%v.%v", verMajor, verMinor),
-		"-dArch="+runtime.GOARCH,
-		"-dSourceDir="+goDir,
-		filepath.Join(win, "installer.wxs"),
-		appfiles,
-	); err != nil {
-		return err
-	}
-
-	msi := filepath.Join(cwd, "msi") // known to internal/task.BuildletStep.BuildWindowsMSI
-	if err := os.Mkdir(msi, 0755); err != nil {
-		return err
-	}
-	fmt.Fprintln(os.Stderr, "running wix light")
-	return runDir(win, filepath.Join(wix, "light"),
-		"-nologo",
-		"-dcl:high",
-		"-ext", "WixUIExtension",
-		"-ext", "WixUtilExtension",
-		"AppFiles.wixobj",
-		"installer.wixobj",
-		"-o", filepath.Join(msi, "go.msi"), // file name irrelevant
-	)
-}
-
-type wixRelease struct {
-	BinaryURL string
-	SHA256    string
-}
-
-var (
-	wixRelease311 = wixRelease{
-		BinaryURL: "https://storage.googleapis.com/go-builder-data/wix311-binaries.zip",
-		SHA256:    "da034c489bd1dd6d8e1623675bf5e899f32d74d6d8312f8dd125a084543193de",
-	}
-	wixRelease314 = wixRelease{
-		BinaryURL: "https://storage.googleapis.com/go-builder-data/wix314-binaries.zip",
-		SHA256:    "34dcbba9952902bfb710161bd45ee2e721ffa878db99f738285a21c9b09c6edb", // WiX v3.14.0.4118 release, SHA 256 of wix314-binaries.zip from https://wixtoolset.org/releases/v3-14-0-4118/.
-	}
-)
-
-// installWix fetches and installs the wix toolkit to the specified path.
-func installWix(wix wixRelease, path string) error {
-	// Fetch wix binary zip file.
-	body, err := httpGet(wix.BinaryURL)
-	if err != nil {
-		return err
-	}
-
-	// Verify sha256.
-	sum := sha256.Sum256(body)
-	if fmt.Sprintf("%x", sum) != wix.SHA256 {
-		return errors.New("sha256 mismatch for wix toolkit")
-	}
-
-	// Unzip to path.
-	zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
-	if err != nil {
-		return err
-	}
-	for _, f := range zr.File {
-		name := filepath.FromSlash(f.Name)
-		err := os.MkdirAll(filepath.Join(path, filepath.Dir(name)), 0755)
-		if err != nil {
-			return err
-		}
-		rc, err := f.Open()
-		if err != nil {
-			return err
-		}
-		b, err := io.ReadAll(rc)
-		rc.Close()
-		if err != nil {
-			return err
-		}
-		err = os.WriteFile(filepath.Join(path, name), b, 0644)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func httpGet(url string) ([]byte, error) {
-	r, err := http.Get(url)
-	if err != nil {
-		return nil, err
-	}
-	body, err := io.ReadAll(r.Body)
-	r.Body.Close()
-	if err != nil {
-		return nil, err
-	}
-	if r.StatusCode != 200 {
-		return nil, errors.New(r.Status)
-	}
-	return body, nil
-}
-
-func run(name string, arg ...string) error {
-	cmd := exec.Command(name, arg...)
-	cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
-	return cmd.Run()
-}
-
-func runDir(dir, name string, arg ...string) error {
-	cmd := exec.Command(name, arg...)
-	cmd.Dir = dir
-	if dir != "" {
-		cmd.Env = append(os.Environ(), "PWD="+dir)
-	}
-	cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
-	return cmd.Run()
-}
-
-func cp(dst, src string) error {
-	sf, err := os.Open(src)
-	if err != nil {
-		return err
-	}
-	defer sf.Close()
-	fi, err := sf.Stat()
-	if err != nil {
-		return err
-	}
-	tmpDst := dst + ".tmp"
-	df, err := os.Create(tmpDst)
-	if err != nil {
-		return err
-	}
-	defer df.Close()
-	// Windows doesn't implement Fchmod.
-	if runtime.GOOS != "windows" {
-		if err := df.Chmod(fi.Mode()); err != nil {
-			return err
-		}
-	}
-	_, err = io.Copy(df, sf)
-	if err != nil {
-		return err
-	}
-	if err := df.Close(); err != nil {
-		return err
-	}
-	if err := os.Rename(tmpDst, dst); err != nil {
-		return err
-	}
-	// Ensure the destination has the same mtime as the source.
-	return os.Chtimes(dst, fi.ModTime(), fi.ModTime())
-}
-
-func ext() string {
-	if runtime.GOOS == "windows" {
-		return ".exe"
-	}
-	return ""
-}
-
-var versionRe = regexp.MustCompile(`^go1\.(\d+(\.\d+)?)`)
-
-// splitVersion splits a Go version string such as "go1.9" or "go1.10.2" (as matched by versionRe)
-// into its parts: major and minor.
-func splitVersion(v string) (major, minor int) {
-	m := versionRe.FindStringSubmatch(v)
-	if m == nil {
-		return
-	}
-	parts := strings.Split(m[1], ".")
-	if len(parts) >= 1 {
-		major, _ = strconv.Atoi(parts[0])
-
-		if len(parts) >= 2 {
-			minor, _ = strconv.Atoi(parts[1])
-		}
-	}
-	return
-}
-
-const storageBase = "https://storage.googleapis.com/go-builder-data/release/"
-
-// writeDataFiles writes the files in the provided map to the provided base
-// directory. If the map value is a URL it fetches the data at that URL and
-// uses it as the file contents.
-func writeDataFiles(data map[string]string, base string) error {
-	for name, body := range data {
-		dst := filepath.Join(base, name)
-		err := os.MkdirAll(filepath.Dir(dst), 0755)
-		if err != nil {
-			return err
-		}
-		b := []byte(body)
-		if strings.HasPrefix(body, storageBase) {
-			b, err = httpGet(body)
-			if err != nil {
-				return err
-			}
-		}
-		// (We really mean 0755 on the next line; some of these files
-		// are executable, and there's no harm in making them all so.)
-		if err := os.WriteFile(dst, b, 0755); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-var windowsData = map[string]string{
-
-	"installer.wxs": `<?xml version="1.0" encoding="UTF-8"?>
-<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
-<!--
-# Copyright 2010 The Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
--->
-
-<?if $(var.Arch) = 386 ?>
-  <?define UpgradeCode = {1C3114EA-08C3-11E1-9095-7FCA4824019B} ?>
-  <?define InstallerVersion="300" ?>
-  <?define SysFolder=SystemFolder ?>
-  <?define ArchProgramFilesFolder="ProgramFilesFolder" ?>
-<?elseif $(var.Arch) = arm64 ?>
-  <?define UpgradeCode = {21ade9a3-3fdd-4ba6-bea6-c85abadc9488} ?>
-  <?define InstallerVersion="500" ?>
-  <?define SysFolder=System64Folder ?>
-  <?define ArchProgramFilesFolder="ProgramFiles64Folder" ?>
-<?else?>
-  <?define UpgradeCode = {22ea7650-4ac6-4001-bf29-f4b8775db1c0} ?>
-  <?define InstallerVersion="300" ?>
-  <?define SysFolder=System64Folder ?>
-  <?define ArchProgramFilesFolder="ProgramFiles64Folder" ?>
-<?endif?>
-
-<Product
-    Id="*"
-    Name="Go Programming Language $(var.Arch) $(var.GoVersion)"
-    Language="1033"
-    Version="$(var.WixGoVersion)"
-    Manufacturer="https://go.dev"
-    UpgradeCode="$(var.UpgradeCode)" >
-
-<Package
-    Id='*'
-    Keywords='Installer'
-    Description="The Go Programming Language Installer"
-    Comments="The Go programming language is an open source project to make programmers more productive."
-    InstallerVersion="$(var.InstallerVersion)"
-    Compressed="yes"
-    InstallScope="perMachine"
-    Languages="1033" />
-
-<Property Id="ARPCOMMENTS" Value="The Go programming language is a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language." />
-<Property Id="ARPCONTACT" Value="golang-nuts@googlegroups.com" />
-<Property Id="ARPHELPLINK" Value="https://go.dev/help" />
-<Property Id="ARPREADME" Value="https://go.dev" />
-<Property Id="ARPURLINFOABOUT" Value="https://go.dev" />
-<Property Id="LicenseAccepted">1</Property>
-<Icon Id="gopher.ico" SourceFile="images\gopher.ico"/>
-<Property Id="ARPPRODUCTICON" Value="gopher.ico" />
-<Property Id="EXISTING_GOLANG_INSTALLED">
-  <RegistrySearch Id="installed" Type="raw" Root="HKCU" Key="Software\GoProgrammingLanguage" Name="installed" />
-</Property>
-<MediaTemplate EmbedCab="yes" CompressionLevel="high" MaximumUncompressedMediaSize="10" />
-<?if $(var.GoMajorVersion) < 21 ?>
-<Condition Message="Windows 7 (with Service Pack 1) or greater required.">
-    ((VersionNT > 601) OR (VersionNT = 601 AND ServicePackLevel >= 1))
-</Condition>
-<?else?>
-<Condition Message="Windows 10 or greater required.">
-<!-- In true MS fashion, Windows 10 pretends to be windows 8.1.
-	See https://learn.microsoft.com/en-us/troubleshoot/windows-client/application-management/versionnt-value-for-windows-10-server .
-	Workarounds exist, but seem difficult/flaky.
-	1) We could build a "bootstrapper" with wix burn, but then we'll be building .exes and there might be implications to that.
-	2) We can try one of the things listed here: https://stackoverflow.com/q/31932646 but that takes us back to https://github.com/wixtoolset/issues/issues/5824 and needing a bootstrapper.
-	So we're stuck with checking for 8.1.
--->
-    (VersionNT >= 603)
-</Condition>
-<?endif?>
-<MajorUpgrade AllowDowngrades="yes" />
-
-<CustomAction
-    Id="SetApplicationRootDirectory"
-    Property="ARPINSTALLLOCATION"
-    Value="[INSTALLDIR]" />
-
-<!-- Define the directory structure and environment variables -->
-<Directory Id="TARGETDIR" Name="SourceDir">
-  <Directory Id="$(var.ArchProgramFilesFolder)">
-    <Directory Id="INSTALLDIR" Name="Go"/>
-  </Directory>
-  <Directory Id="ProgramMenuFolder">
-    <Directory Id="GoProgramShortcutsDir" Name="Go Programming Language"/>
-  </Directory>
-  <Directory Id="EnvironmentEntries">
-    <Directory Id="GoEnvironmentEntries" Name="Go Programming Language"/>
-  </Directory>
-</Directory>
-
-<!-- Programs Menu Shortcuts -->
-<DirectoryRef Id="GoProgramShortcutsDir">
-  <Component Id="Component_GoProgramShortCuts" Guid="{f5fbfb5e-6c5c-423b-9298-21b0e3c98f4b}">
-    <Shortcut
-        Id="UninstallShortcut"
-        Name="Uninstall Go"
-        Description="Uninstalls Go and all of its components"
-        Target="[$(var.SysFolder)]msiexec.exe"
-        Arguments="/x [ProductCode]" />
-    <RemoveFolder
-        Id="GoProgramShortcutsDir"
-        On="uninstall" />
-    <RegistryValue
-        Root="HKCU"
-        Key="Software\GoProgrammingLanguage"
-        Name="ShortCuts"
-        Type="integer"
-        Value="1"
-        KeyPath="yes" />
-  </Component>
-</DirectoryRef>
-
-<!-- Registry & Environment Settings -->
-<DirectoryRef Id="GoEnvironmentEntries">
-  <Component Id="Component_GoEnvironment" Guid="{3ec7a4d5-eb08-4de7-9312-2df392c45993}">
-    <RegistryKey
-        Root="HKCU"
-        Key="Software\GoProgrammingLanguage">
-            <RegistryValue
-                Name="installed"
-                Type="integer"
-                Value="1"
-                KeyPath="yes" />
-            <RegistryValue
-                Name="installLocation"
-                Type="string"
-                Value="[INSTALLDIR]" />
-    </RegistryKey>
-    <Environment
-        Id="GoPathEntry"
-        Action="set"
-        Part="last"
-        Name="PATH"
-        Permanent="no"
-        System="yes"
-        Value="[INSTALLDIR]bin" />
-    <Environment
-        Id="UserGoPath"
-        Action="create"
-        Name="GOPATH"
-        Permanent="no"
-        Value="%USERPROFILE%\go" />
-    <Environment
-        Id="UserGoPathEntry"
-        Action="set"
-        Part="last"
-        Name="PATH"
-        Permanent="no"
-        Value="%USERPROFILE%\go\bin" />
-    <RemoveFolder
-        Id="GoEnvironmentEntries"
-        On="uninstall" />
-  </Component>
-</DirectoryRef>
-
-<!-- Install the files -->
-<Feature
-    Id="GoTools"
-    Title="Go"
-    Level="1">
-      <ComponentRef Id="Component_GoEnvironment" />
-      <ComponentGroupRef Id="AppFiles" />
-      <ComponentRef Id="Component_GoProgramShortCuts" />
-</Feature>
-
-<!-- Update the environment -->
-<InstallExecuteSequence>
-    <Custom Action="SetApplicationRootDirectory" Before="InstallFinalize" />
-</InstallExecuteSequence>
-
-<!-- Notify top level applications of the new PATH variable (go.dev/issue/18680)  -->
-<CustomActionRef Id="WixBroadcastEnvironmentChange" />
-
-<!-- Include the user interface -->
-<WixVariable Id="WixUILicenseRtf" Value="LICENSE.rtf" />
-<WixVariable Id="WixUIBannerBmp" Value="images\Banner.jpg" />
-<WixVariable Id="WixUIDialogBmp" Value="images\Dialog.jpg" />
-<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
-<UIRef Id="Golang_InstallDir" />
-<UIRef Id="WixUI_ErrorProgressText" />
-
-</Product>
-<Fragment>
-  <!--
-    The installer steps are modified so we can get user confirmation to uninstall an existing golang installation.
-
-    WelcomeDlg  [not installed]  =>                  LicenseAgreementDlg => InstallDirDlg  ..
-                [installed]      => OldVersionDlg => LicenseAgreementDlg => InstallDirDlg  ..
-  -->
-  <UI Id="Golang_InstallDir">
-    <!-- style -->
-    <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
-    <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
-    <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
-
-    <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
-    <Property Id="WixUI_Mode" Value="InstallDir" />
-
-    <!-- dialogs -->
-    <DialogRef Id="BrowseDlg" />
-    <DialogRef Id="DiskCostDlg" />
-    <DialogRef Id="ErrorDlg" />
-    <DialogRef Id="FatalError" />
-    <DialogRef Id="FilesInUse" />
-    <DialogRef Id="MsiRMFilesInUse" />
-    <DialogRef Id="PrepareDlg" />
-    <DialogRef Id="ProgressDlg" />
-    <DialogRef Id="ResumeDlg" />
-    <DialogRef Id="UserExit" />
-    <Dialog Id="OldVersionDlg" Width="240" Height="95" Title="[ProductName] Setup" NoMinimize="yes">
-      <Control Id="Text" Type="Text" X="28" Y="15" Width="194" Height="50">
-        <Text>A previous version of Go Programming Language is currently installed. By continuing the installation this version will be uninstalled. Do you want to continue?</Text>
-      </Control>
-      <Control Id="Exit" Type="PushButton" X="123" Y="67" Width="62" Height="17"
-        Default="yes" Cancel="yes" Text="No, Exit">
-        <Publish Event="EndDialog" Value="Exit">1</Publish>
-      </Control>
-      <Control Id="Next" Type="PushButton" X="55" Y="67" Width="62" Height="17" Text="Yes, Uninstall">
-        <Publish Event="EndDialog" Value="Return">1</Publish>
-      </Control>
-    </Dialog>
-
-    <!-- wizard steps -->
-    <Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
-    <Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
-
-    <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
-
-    <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="OldVersionDlg"><![CDATA[EXISTING_GOLANG_INSTALLED << "#1"]]> </Publish>
-    <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg"><![CDATA[NOT (EXISTING_GOLANG_INSTALLED << "#1")]]></Publish>
-
-    <Publish Dialog="OldVersionDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
-
-    <Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
-    <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">LicenseAccepted = "1"</Publish>
-
-    <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
-    <Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
-    <Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
-    <Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
-    <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
-    <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
-    <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
-
-    <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">NOT Installed</Publish>
-    <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish>
-    <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish>
-
-    <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
-
-    <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
-    <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
-    <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
-
-    <Property Id="ARPNOMODIFY" Value="1" />
-  </UI>
-
-  <UIRef Id="WixUI_Common" />
-</Fragment>
-</Wix>
-`,
-
-	"LICENSE.rtf":           storageBase + "windows/LICENSE.rtf",
-	"images/Banner.jpg":     storageBase + "windows/Banner.jpg",
-	"images/Dialog.jpg":     storageBase + "windows/Dialog.jpg",
-	"images/DialogLeft.jpg": storageBase + "windows/DialogLeft.jpg",
-	"images/gopher.ico":     storageBase + "windows/gopher.ico",
-}
diff --git a/internal/task/releaselet/releaselet_test.go b/internal/task/releaselet/releaselet_test.go
deleted file mode 100644
index f37ae2a..0000000
--- a/internal/task/releaselet/releaselet_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package main
-
-import (
-	"os"
-	"strings"
-	"testing"
-)
-
-func TestSplitVersion(t *testing.T) {
-	// Test splitVersion.
-	for _, tt := range []struct {
-		v            string
-		minor, patch int
-	}{
-		{"go1", 0, 0},
-		{"go1.34", 34, 0},
-		{"go1.34.7", 34, 7},
-	} {
-		minor, patch := splitVersion(tt.v)
-		if minor != tt.minor || patch != tt.patch {
-			t.Errorf("splitVersion(%q) = %v, %v; want %v, %v",
-				tt.v, minor, patch, tt.minor, tt.patch)
-		}
-	}
-}
-
-func TestSingleFile(t *testing.T) {
-	files, err := os.ReadDir(".")
-	if err != nil {
-		t.Fatal(err)
-	}
-	for _, f := range files {
-		if f.Name() == "releaselet.go" ||
-			f.Name() == "README.md" ||
-			strings.HasSuffix(f.Name(), "_test.go") {
-			continue
-		}
-		t.Errorf("releaselet should be a single file, found %v", f.Name())
-	}
-}