golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/installer/darwinpkg/darwinpkg.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package darwinpkg encodes the process of building a macOS PKG
     6  // installer from the given Go toolchain .tar.gz binary archive.
     7  package darwinpkg
     8  
     9  import (
    10  	"bytes"
    11  	"context"
    12  	"embed"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"io/fs"
    17  	"log"
    18  	"os"
    19  	"os/exec"
    20  	"path/filepath"
    21  	"strings"
    22  	"text/template"
    23  
    24  	"golang.org/x/build/internal/untar"
    25  )
    26  
    27  // InstallerOptions holds options for constructing the installer.
    28  type InstallerOptions struct {
    29  	GOARCH string // The target GOARCH.
    30  	// MinMacOSVersion is the minimum required system.version.ProductVersion.
    31  	// For example, "11" for macOS 11 Big Sur, "10.15" for macOS 10.15 Catalina, etc.
    32  	MinMacOSVersion string
    33  }
    34  
    35  // ConstructInstaller constructs an installer for the provided Go toolchain .tar.gz
    36  // binary archive using workDir as a working directory, and returns the output path.
    37  //
    38  // It's intended to run on a macOS system, with Xcode tools available in $PATH.
    39  func ConstructInstaller(_ context.Context, workDir, tgzPath string, opt InstallerOptions) (pkgPath string, _ error) {
    40  	var errs []error
    41  	for _, dep := range [...]string{"pkgbuild", "productbuild"} {
    42  		if _, err := exec.LookPath(dep); err != nil {
    43  			errs = append(errs, fmt.Errorf("dependency %q is not in PATH", dep))
    44  		}
    45  	}
    46  	if opt.GOARCH == "" {
    47  		errs = append(errs, fmt.Errorf("GOARCH is empty"))
    48  	}
    49  	if opt.MinMacOSVersion == "" {
    50  		errs = append(errs, fmt.Errorf("MinMacOSVersion is empty"))
    51  	}
    52  	if err := errors.Join(errs...); err != nil {
    53  		return "", err
    54  	}
    55  
    56  	oldDir, err := os.Getwd()
    57  	if err != nil {
    58  		panic(err)
    59  	}
    60  	if err := os.Chdir(workDir); err != nil {
    61  		panic(err)
    62  	}
    63  	defer func() {
    64  		if err := os.Chdir(oldDir); err != nil {
    65  			panic(err)
    66  		}
    67  	}()
    68  
    69  	fmt.Println("Building inner .pkg with pkgbuild.")
    70  	run("mkdir", "pkg-intermediate")
    71  	putTar(tgzPath, "pkg-root/usr/local")
    72  	put("/usr/local/go/bin\n", "pkg-root/etc/paths.d/go", 0644)
    73  	put(`#!/bin/bash
    74  
    75  GOROOT=/usr/local/go
    76  echo "Removing previous installation"
    77  if [ -d $GOROOT ]; then
    78  	rm -r $GOROOT
    79  fi
    80  `, "pkg-scripts/preinstall", 0755)
    81  	put(`#!/bin/bash
    82  
    83  GOROOT=/usr/local/go
    84  echo "Fixing permissions"
    85  cd $GOROOT
    86  find . -exec chmod ugo+r \{\} \;
    87  find bin -exec chmod ugo+rx \{\} \;
    88  find . -type d -exec chmod ugo+rx \{\} \;
    89  chmod o-w .
    90  `, "pkg-scripts/postinstall", 0755)
    91  	version := readVERSION("pkg-root/usr/local/go")
    92  	run("pkgbuild",
    93  		"--identifier=org.golang.go",
    94  		"--version", version,
    95  		"--scripts=pkg-scripts",
    96  		"--root=pkg-root",
    97  		"pkg-intermediate/org.golang.go.pkg",
    98  	)
    99  
   100  	fmt.Println("\nBuilding outer .pkg with productbuild.")
   101  	run("mkdir", "pkg-out")
   102  	bg, err := darwinPKGBackground(opt.GOARCH)
   103  	if err != nil {
   104  		log.Fatalln("darwinPKGBackground:", err)
   105  	}
   106  	put(string(bg), "pkg-resources/background.png", 0644)
   107  	var buf bytes.Buffer
   108  	distData := darwinDistData{
   109  		HostArchs: map[string]string{"amd64": "x86_64", "arm64": "arm64"}[opt.GOARCH],
   110  		MinOS:     opt.MinMacOSVersion,
   111  	}
   112  	if err := darwinDistTmpl.ExecuteTemplate(&buf, "dist.xml", distData); err != nil {
   113  		log.Fatalln("darwinDistTmpl.ExecuteTemplate:", err)
   114  	}
   115  	put(buf.String(), "pkg-distribution", 0644)
   116  	run("productbuild",
   117  		"--distribution=pkg-distribution",
   118  		"--resources=pkg-resources",
   119  		"--package-path=pkg-intermediate",
   120  		"pkg-out/"+version+"-unsigned.pkg",
   121  	)
   122  
   123  	return filepath.Join(workDir, "pkg-out", version+"-unsigned.pkg"), nil
   124  }
   125  
   126  //go:embed _data
   127  var darwinPKGData embed.FS
   128  
   129  func darwinPKGBackground(goarch string) ([]byte, error) {
   130  	switch goarch {
   131  	case "arm64":
   132  		return darwinPKGData.ReadFile("_data/blue-bg.png")
   133  	case "amd64":
   134  		return darwinPKGData.ReadFile("_data/brown-bg.png")
   135  	default:
   136  		return nil, fmt.Errorf("no background for GOARCH %q", goarch)
   137  	}
   138  }
   139  
   140  var darwinDistTmpl = template.Must(template.New("").ParseFS(darwinPKGData, "_data/dist.xml"))
   141  
   142  type darwinDistData struct {
   143  	HostArchs string // hostArchitectures option value.
   144  	MinOS     string // Minimum required system.version.ProductVersion.
   145  }
   146  
   147  func put(content, dst string, perm fs.FileMode) {
   148  	err := os.MkdirAll(filepath.Dir(dst), 0755)
   149  	if err != nil {
   150  		panic(err)
   151  	}
   152  	f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
   153  	if err != nil {
   154  		panic(err)
   155  	}
   156  	_, err = io.WriteString(f, content)
   157  	if err != nil {
   158  		panic(err)
   159  	}
   160  	err = f.Close()
   161  	if err != nil {
   162  		panic(err)
   163  	}
   164  }
   165  
   166  func putTar(tgz, dir string) {
   167  	f, err := os.Open(tgz)
   168  	if err != nil {
   169  		panic(err)
   170  	}
   171  	err = untar.Untar(f, dir)
   172  	if err != nil {
   173  		panic(err)
   174  	}
   175  	err = f.Close()
   176  	if err != nil {
   177  		panic(err)
   178  	}
   179  }
   180  
   181  // run runs the command and requires that it succeeds.
   182  // If not, it logs the failure and exits with a non-zero code.
   183  // It prints the command line.
   184  func run(name string, args ...string) {
   185  	fmt.Printf("$ %s %s\n", name, strings.Join(args, " "))
   186  	out, err := exec.Command(name, args...).CombinedOutput()
   187  	if err != nil {
   188  		log.Fatalf("command failed: %v\n%s", err, out)
   189  	}
   190  }
   191  
   192  // readVERSION reads the VERSION file and
   193  // returns the first line of the file, the Go version.
   194  func readVERSION(goroot string) (version string) {
   195  	b, err := os.ReadFile(filepath.Join(goroot, "VERSION"))
   196  	if err != nil {
   197  		panic(err)
   198  	}
   199  	version, _, _ = strings.Cut(string(b), "\n")
   200  	return version
   201  }