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// Distpack creates the tgz and zip files for a Go distribution.
6// It writes into GOROOT/pkg/distpack:
7//
8//   - a binary distribution (tgz or zip) for the current GOOS and GOARCH
9//   - a source distribution that is independent of GOOS/GOARCH
10//   - the module mod, info, and zip files for a distribution in module form
11//     (as used by GOTOOLCHAIN support in the go command).
12//
13// Distpack is typically invoked by the -distpack flag to make.bash.
14// A cross-compiled distribution for goos/goarch can be built using:
15//
16//	GOOS=goos GOARCH=goarch ./make.bash -distpack
17//
18// To test that the module downloads are usable with the go command:
19//
20//	./make.bash -distpack
21//	mkdir -p /tmp/goproxy/golang.org/toolchain/
22//	ln -sf $(pwd)/../pkg/distpack /tmp/goproxy/golang.org/toolchain/@v
23//	GOPROXY=file:///tmp/goproxy GOTOOLCHAIN=$(sed 1q ../VERSION) gotip version
24//
25// gotip can be replaced with an older released Go version once there is one.
26// It just can't be the one make.bash built, because it knows it is already that
27// version and will skip the download.
28package main
29
30import (
31	"archive/tar"
32	"archive/zip"
33	"compress/flate"
34	"compress/gzip"
35	"crypto/sha256"
36	"flag"
37	"fmt"
38	"io"
39	"io/fs"
40	"log"
41	"os"
42	"path"
43	"path/filepath"
44	"runtime"
45	"strings"
46	"time"
47
48	"cmd/internal/telemetry/counter"
49)
50
51func usage() {
52	fmt.Fprintf(os.Stderr, "usage: distpack\n")
53	os.Exit(2)
54}
55
56const (
57	modPath          = "golang.org/toolchain"
58	modVersionPrefix = "v0.0.1"
59)
60
61var (
62	goroot     string
63	gohostos   string
64	gohostarch string
65	goos       string
66	goarch     string
67)
68
69func main() {
70	log.SetPrefix("distpack: ")
71	log.SetFlags(0)
72	counter.Open()
73	flag.Usage = usage
74	flag.Parse()
75	counter.Inc("distpack/invocations")
76	counter.CountFlags("distpack/flag:", *flag.CommandLine)
77	if flag.NArg() != 0 {
78		usage()
79	}
80
81	// Load context.
82	goroot = runtime.GOROOT()
83	if goroot == "" {
84		log.Fatalf("missing $GOROOT")
85	}
86	gohostos = runtime.GOOS
87	gohostarch = runtime.GOARCH
88	goos = os.Getenv("GOOS")
89	if goos == "" {
90		goos = gohostos
91	}
92	goarch = os.Getenv("GOARCH")
93	if goarch == "" {
94		goarch = gohostarch
95	}
96	goosUnderGoarch := goos + "_" + goarch
97	goosDashGoarch := goos + "-" + goarch
98	exe := ""
99	if goos == "windows" {
100		exe = ".exe"
101	}
102	version, versionTime := readVERSION(goroot)
103
104	// Start with files from GOROOT, filtering out non-distribution files.
105	base, err := NewArchive(goroot)
106	if err != nil {
107		log.Fatal(err)
108	}
109	base.SetTime(versionTime)
110	base.SetMode(mode)
111	base.Remove(
112		".git/**",
113		".gitattributes",
114		".github/**",
115		".gitignore",
116		"VERSION.cache",
117		"misc/cgo/*/_obj/**",
118		"**/.DS_Store",
119		"**/*.exe~", // go.dev/issue/23894
120		// Generated during make.bat/make.bash.
121		"src/cmd/dist/dist",
122		"src/cmd/dist/dist.exe",
123	)
124
125	// The source distribution removes files generated during the release build.
126	// See ../dist/build.go's deptab.
127	srcArch := base.Clone()
128	srcArch.Remove(
129		"bin/**",
130		"pkg/**",
131
132		// Generated during cmd/dist. See ../dist/build.go:/gentab.
133		"src/cmd/go/internal/cfg/zdefaultcc.go",
134		"src/go/build/zcgo.go",
135		"src/runtime/internal/sys/zversion.go",
136		"src/time/tzdata/zzipdata.go",
137
138		// Generated during cmd/dist by bootstrapBuildTools.
139		"src/cmd/cgo/zdefaultcc.go",
140		"src/cmd/internal/objabi/zbootstrap.go",
141		"src/internal/buildcfg/zbootstrap.go",
142
143		// Generated by earlier versions of cmd/dist .
144		"src/cmd/go/internal/cfg/zosarch.go",
145	)
146	srcArch.AddPrefix("go")
147	testSrc(srcArch)
148
149	// The binary distribution includes only a subset of bin and pkg.
150	binArch := base.Clone()
151	binArch.Filter(func(name string) bool {
152		// Discard bin/ for now, will add back later.
153		if strings.HasPrefix(name, "bin/") {
154			return false
155		}
156		// Discard most of pkg.
157		if strings.HasPrefix(name, "pkg/") {
158			// Keep pkg/include.
159			if strings.HasPrefix(name, "pkg/include/") {
160				return true
161			}
162			// Discard other pkg except pkg/tool.
163			if !strings.HasPrefix(name, "pkg/tool/") {
164				return false
165			}
166			// Inside pkg/tool, keep only $GOOS_$GOARCH.
167			if !strings.HasPrefix(name, "pkg/tool/"+goosUnderGoarch+"/") {
168				return false
169			}
170			// Inside pkg/tool/$GOOS_$GOARCH, discard helper tools.
171			switch strings.TrimSuffix(path.Base(name), ".exe") {
172			case "api", "dist", "distpack", "metadata":
173				return false
174			}
175		}
176		return true
177	})
178
179	// Add go and gofmt to bin, using cross-compiled binaries
180	// if this is a cross-compiled distribution.
181	binExes := []string{
182		"go",
183		"gofmt",
184	}
185	crossBin := "bin"
186	if goos != gohostos || goarch != gohostarch {
187		crossBin = "bin/" + goosUnderGoarch
188	}
189	for _, b := range binExes {
190		name := "bin/" + b + exe
191		src := filepath.Join(goroot, crossBin, b+exe)
192		info, err := os.Stat(src)
193		if err != nil {
194			log.Fatal(err)
195		}
196		binArch.Add(name, src, info)
197	}
198	binArch.Sort()
199	binArch.SetTime(versionTime) // fix added files
200	binArch.SetMode(mode)        // fix added files
201
202	zipArch := binArch.Clone()
203	zipArch.AddPrefix("go")
204	testZip(zipArch)
205
206	// The module distribution is the binary distribution with unnecessary files removed
207	// and file names using the necessary prefix for the module.
208	modArch := binArch.Clone()
209	modArch.Remove(
210		"api/**",
211		"doc/**",
212		"misc/**",
213		"test/**",
214	)
215	modVers := modVersionPrefix + "-" + version + "." + goosDashGoarch
216	modArch.AddPrefix(modPath + "@" + modVers)
217	modArch.RenameGoMod()
218	modArch.Sort()
219	testMod(modArch)
220
221	// distpack returns the full path to name in the distpack directory.
222	distpack := func(name string) string {
223		return filepath.Join(goroot, "pkg/distpack", name)
224	}
225	if err := os.MkdirAll(filepath.Join(goroot, "pkg/distpack"), 0777); err != nil {
226		log.Fatal(err)
227	}
228
229	writeTgz(distpack(version+".src.tar.gz"), srcArch)
230
231	if goos == "windows" {
232		writeZip(distpack(version+"."+goos+"-"+goarch+".zip"), zipArch)
233	} else {
234		writeTgz(distpack(version+"."+goos+"-"+goarch+".tar.gz"), zipArch)
235	}
236
237	writeZip(distpack(modVers+".zip"), modArch)
238	writeFile(distpack(modVers+".mod"),
239		[]byte(fmt.Sprintf("module %s\n", modPath)))
240	writeFile(distpack(modVers+".info"),
241		[]byte(fmt.Sprintf("{%q:%q, %q:%q}\n",
242			"Version", modVers,
243			"Time", versionTime.Format(time.RFC3339))))
244}
245
246// mode computes the mode for the given file name.
247func mode(name string, _ fs.FileMode) fs.FileMode {
248	if strings.HasPrefix(name, "bin/") ||
249		strings.HasPrefix(name, "pkg/tool/") ||
250		strings.HasSuffix(name, ".bash") ||
251		strings.HasSuffix(name, ".sh") ||
252		strings.HasSuffix(name, ".pl") ||
253		strings.HasSuffix(name, ".rc") {
254		return 0o755
255	} else if ok, _ := amatch("**/go_?*_?*_exec", name); ok {
256		return 0o755
257	}
258	return 0o644
259}
260
261// readVERSION reads the VERSION file.
262// The first line of the file is the Go version.
263// Additional lines are 'key value' pairs setting other data.
264// The only valid key at the moment is 'time', which sets the modification time for file archives.
265func readVERSION(goroot string) (version string, t time.Time) {
266	data, err := os.ReadFile(filepath.Join(goroot, "VERSION"))
267	if err != nil {
268		log.Fatal(err)
269	}
270	version, rest, _ := strings.Cut(string(data), "\n")
271	for _, line := range strings.Split(rest, "\n") {
272		f := strings.Fields(line)
273		if len(f) == 0 {
274			continue
275		}
276		switch f[0] {
277		default:
278			log.Fatalf("VERSION: unexpected line: %s", line)
279		case "time":
280			if len(f) != 2 {
281				log.Fatalf("VERSION: unexpected time line: %s", line)
282			}
283			t, err = time.ParseInLocation(time.RFC3339, f[1], time.UTC)
284			if err != nil {
285				log.Fatalf("VERSION: bad time: %s", err)
286			}
287		}
288	}
289	return version, t
290}
291
292// writeFile writes a file with the given name and data or fatals.
293func writeFile(name string, data []byte) {
294	if err := os.WriteFile(name, data, 0666); err != nil {
295		log.Fatal(err)
296	}
297	reportHash(name)
298}
299
300// check panics if err is not nil. Otherwise it returns x.
301// It is only meant to be used in a function that has deferred
302// a function to recover appropriately from the panic.
303func check[T any](x T, err error) T {
304	check1(err)
305	return x
306}
307
308// check1 panics if err is not nil.
309// It is only meant to be used in a function that has deferred
310// a function to recover appropriately from the panic.
311func check1(err error) {
312	if err != nil {
313		panic(err)
314	}
315}
316
317// writeTgz writes the archive in tgz form to the file named name.
318func writeTgz(name string, a *Archive) {
319	out, err := os.Create(name)
320	if err != nil {
321		log.Fatal(err)
322	}
323
324	var f File
325	defer func() {
326		if err := recover(); err != nil {
327			extra := ""
328			if f.Name != "" {
329				extra = " " + f.Name
330			}
331			log.Fatalf("writing %s%s: %v", name, extra, err)
332		}
333	}()
334
335	zw := check(gzip.NewWriterLevel(out, gzip.BestCompression))
336	tw := tar.NewWriter(zw)
337
338	// Find the mode and mtime to use for directory entries,
339	// based on the mode and mtime of the first file we see.
340	// We know that modes and mtimes are uniform across the archive.
341	var dirMode fs.FileMode
342	var mtime time.Time
343	for _, f := range a.Files {
344		dirMode = fs.ModeDir | f.Mode | (f.Mode&0444)>>2 // copy r bits down to x bits
345		mtime = f.Time
346		break
347	}
348
349	// mkdirAll ensures that the tar file contains directory
350	// entries for dir and all its parents. Some programs reading
351	// these tar files expect that. See go.dev/issue/61862.
352	haveDir := map[string]bool{".": true}
353	var mkdirAll func(string)
354	mkdirAll = func(dir string) {
355		if dir == "/" {
356			panic("mkdirAll /")
357		}
358		if haveDir[dir] {
359			return
360		}
361		haveDir[dir] = true
362		mkdirAll(path.Dir(dir))
363		df := &File{
364			Name: dir + "/",
365			Time: mtime,
366			Mode: dirMode,
367		}
368		h := check(tar.FileInfoHeader(df.Info(), ""))
369		h.Name = dir + "/"
370		if err := tw.WriteHeader(h); err != nil {
371			panic(err)
372		}
373	}
374
375	for _, f = range a.Files {
376		h := check(tar.FileInfoHeader(f.Info(), ""))
377		mkdirAll(path.Dir(f.Name))
378		h.Name = f.Name
379		if err := tw.WriteHeader(h); err != nil {
380			panic(err)
381		}
382		r := check(os.Open(f.Src))
383		check(io.Copy(tw, r))
384		check1(r.Close())
385	}
386	f.Name = ""
387	check1(tw.Close())
388	check1(zw.Close())
389	check1(out.Close())
390	reportHash(name)
391}
392
393// writeZip writes the archive in zip form to the file named name.
394func writeZip(name string, a *Archive) {
395	out, err := os.Create(name)
396	if err != nil {
397		log.Fatal(err)
398	}
399
400	var f File
401	defer func() {
402		if err := recover(); err != nil {
403			extra := ""
404			if f.Name != "" {
405				extra = " " + f.Name
406			}
407			log.Fatalf("writing %s%s: %v", name, extra, err)
408		}
409	}()
410
411	zw := zip.NewWriter(out)
412	zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
413		return flate.NewWriter(out, flate.BestCompression)
414	})
415	for _, f = range a.Files {
416		h := check(zip.FileInfoHeader(f.Info()))
417		h.Name = f.Name
418		h.Method = zip.Deflate
419		w := check(zw.CreateHeader(h))
420		r := check(os.Open(f.Src))
421		check(io.Copy(w, r))
422		check1(r.Close())
423	}
424	f.Name = ""
425	check1(zw.Close())
426	check1(out.Close())
427	reportHash(name)
428}
429
430func reportHash(name string) {
431	f, err := os.Open(name)
432	if err != nil {
433		log.Fatal(err)
434	}
435	h := sha256.New()
436	io.Copy(h, f)
437	f.Close()
438	fmt.Printf("distpack: %x %s\n", h.Sum(nil)[:8], filepath.Base(name))
439}
440