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 }