golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/gorebuild/report.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 main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"log"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"runtime/debug"
    15  	"sort"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  )
    20  
    21  // A Report is the report about this reproduction attempt.
    22  // It also holds unexported state for use during the attempt.
    23  type Report struct {
    24  	Version    string // module@version of gorebuild command
    25  	GoVersion  string // version of go command gorebuild was built with
    26  	GOOS       string
    27  	GOARCH     string
    28  	Start      time.Time    // time reproduction started
    29  	End        time.Time    // time reproduction ended
    30  	Work       string       // work directory
    31  	Full       bool         // full bootstrap back to Go 1.4
    32  	Bootstraps []*Bootstrap // bootstrap toolchains used
    33  	Releases   []*Release   // releases reproduced
    34  	Log        Log
    35  
    36  	dl []*DLRelease // information from go.dev/dl
    37  }
    38  
    39  // A Bootstrap describes the result of building or obtaining a bootstrap toolchain.
    40  type Bootstrap struct {
    41  	Version string
    42  	Dir     string
    43  	Err     error
    44  	Log     Log
    45  }
    46  
    47  // A Release describes results for files from a single release of Go.
    48  type Release struct {
    49  	Version string // Go version string "go1.21.3"
    50  	Log     Log
    51  	dl      *DLRelease
    52  
    53  	mu    sync.Mutex
    54  	Files []*File // Files reproduced
    55  }
    56  
    57  // A File describes the result of reproducing a single file.
    58  type File struct {
    59  	Name   string // Name of file on go.dev/dl ("go1.21.3-linux-amd64.tar.gz")
    60  	GOOS   string
    61  	GOARCH string
    62  	SHA256 string // SHA256 hex of file
    63  	Log    Log
    64  	dl     *DLFile
    65  
    66  	cache bool
    67  	mu    sync.Mutex
    68  	data  []byte
    69  }
    70  
    71  // A Log contains timestamped log messages as well as an overall
    72  // result status derived from them.
    73  type Log struct {
    74  	Name string
    75  
    76  	// mu must be held when using the Log from multiple goroutines.
    77  	// It is OK not to hold mu when there is only a single goroutine accessing
    78  	// the data, such as during json.Marshal or json.Unmarshal.
    79  	mu       sync.Mutex
    80  	Messages []Message
    81  	Status   Status
    82  }
    83  
    84  // A Status reports the overall result of the report, version, or file:
    85  // FAIL, PASS, or SKIP.
    86  type Status string
    87  
    88  const (
    89  	FAIL Status = "FAIL"
    90  	PASS Status = "PASS"
    91  	SKIP Status = "SKIP"
    92  )
    93  
    94  // A Message is a single log message.
    95  type Message struct {
    96  	Time time.Time
    97  	Text string
    98  }
    99  
   100  // Printf adds a new message to the log.
   101  // If the message begins with FAIL:, PASS:, or SKIP:,
   102  // the status is updated accordingly.
   103  func (l *Log) Printf(format string, args ...any) {
   104  	l.mu.Lock()
   105  	defer l.mu.Unlock()
   106  
   107  	text := fmt.Sprintf(format, args...)
   108  	text = strings.TrimRight(text, "\n")
   109  	now := time.Now()
   110  	l.Messages = append(l.Messages, Message{now, text})
   111  
   112  	if strings.HasPrefix(format, "FAIL:") {
   113  		l.Status = FAIL
   114  	} else if strings.HasPrefix(format, "PASS:") && l.Status != FAIL {
   115  		l.Status = PASS
   116  	} else if strings.HasPrefix(format, "SKIP:") && l.Status == "" {
   117  		l.Status = SKIP
   118  	}
   119  
   120  	prefix := ""
   121  	if l.Name != "" {
   122  		prefix = "[" + l.Name + "] "
   123  	}
   124  	fmt.Fprintf(os.Stderr, "%s %s%s\n", now.Format("15:04:05.000"), prefix, text)
   125  }
   126  
   127  // Run runs the rebuilds indicated by args and returns the resulting report.
   128  func Run(args []string) *Report {
   129  	r := &Report{
   130  		Version:   "(unknown)",
   131  		GoVersion: runtime.Version(),
   132  		GOOS:      runtime.GOOS,
   133  		GOARCH:    runtime.GOARCH,
   134  		Start:     time.Now(),
   135  		Full:      runtime.GOOS == "linux" && runtime.GOARCH == "amd64",
   136  	}
   137  	defer func() {
   138  		r.End = time.Now()
   139  	}()
   140  	if info, ok := debug.ReadBuildInfo(); ok {
   141  		m := &info.Main
   142  		if m.Replace != nil {
   143  			m = m.Replace
   144  		}
   145  		r.Version = m.Path + "@" + m.Version
   146  	}
   147  
   148  	var err error
   149  	defer func() {
   150  		if err != nil {
   151  			r.Log.Printf("FAIL: %v", err)
   152  		}
   153  	}()
   154  
   155  	r.Work, err = os.MkdirTemp("", "gorebuild-")
   156  	if err != nil {
   157  		return r
   158  	}
   159  
   160  	r.dl, err = DLReleases(&r.Log)
   161  	if err != nil {
   162  		return r
   163  	}
   164  
   165  	// Allocate files for all the arguments.
   166  	if len(args) == 0 {
   167  		args = []string{""}
   168  	}
   169  	for _, arg := range args {
   170  		sys, vers, ok := strings.Cut(arg, "@")
   171  		versions := []string{vers}
   172  		if !ok {
   173  			versions = defaultVersions(r.dl)
   174  		}
   175  		for _, version := range versions {
   176  			rel := r.Release(version)
   177  			if rel == nil {
   178  				r.Log.Printf("FAIL: unknown version %q", version)
   179  				continue
   180  			}
   181  			r.File(rel, rel.Version+".src.tar.gz", "", "").cache = true
   182  			for _, f := range rel.dl.Files {
   183  				if f.Kind == "source" || sys == "" || sys == f.GOOS+"-"+f.GOARCH {
   184  					r.File(rel, f.Name, f.GOOS, f.GOARCH).dl = f
   185  					if f.GOOS != "" && f.GOARCH != "" {
   186  						mod := "v0.0.1-" + rel.Version + "." + f.GOOS + "-" + f.GOARCH
   187  						r.File(rel, mod+".info", f.GOOS, f.GOARCH)
   188  						r.File(rel, mod+".mod", f.GOOS, f.GOARCH)
   189  						r.File(rel, mod+".zip", f.GOOS, f.GOARCH)
   190  					}
   191  				}
   192  			}
   193  		}
   194  	}
   195  
   196  	// Do the work.
   197  	// Fetch or build the bootstraps single-threaded.
   198  	for _, rel := range r.Releases {
   199  		// If BootstrapVersion fails, the parallel loop will report that.
   200  		bver, _ := BootstrapVersion(rel.Version)
   201  		if bver != "" {
   202  			r.BootstrapDir(bver)
   203  		}
   204  	}
   205  
   206  	// Run every file in its own goroutine.
   207  	// Limit parallelism with channel.
   208  	N := *pFlag
   209  	if N < 1 {
   210  		log.Fatalf("invalid parallelism -p=%d", *pFlag)
   211  	}
   212  	limit := make(chan int, N)
   213  	for i := 0; i < N; i++ {
   214  		limit <- 1
   215  	}
   216  	for _, rel := range r.Releases {
   217  		rel := rel
   218  		// Download source code.
   219  		src, err := GerritTarGz(&rel.Log, "go", "refs/tags/"+rel.Version)
   220  		if err != nil {
   221  			rel.Log.Printf("FAIL: downloading source: %v", err)
   222  			continue
   223  		}
   224  
   225  		// Reproduce all the files.
   226  		for _, file := range rel.Files {
   227  			file := file
   228  			<-limit
   229  			go func() {
   230  				defer func() { limit <- 1 }()
   231  				r.ReproFile(rel, file, src)
   232  			}()
   233  		}
   234  	}
   235  
   236  	// Wait for goroutines to finish.
   237  	for i := 0; i < N; i++ {
   238  		<-limit
   239  	}
   240  
   241  	// Collect results.
   242  	// Sort the list of work for nicer presentation.
   243  	if r.Log.Status != FAIL {
   244  		r.Log.Status = PASS
   245  	}
   246  	sort.Slice(r.Releases, func(i, j int) bool { return Compare(r.Releases[i].Version, r.Releases[j].Version) > 0 })
   247  	for _, rel := range r.Releases {
   248  		if rel.Log.Status != FAIL {
   249  			rel.Log.Status = PASS
   250  		}
   251  		sort.Slice(rel.Files, func(i, j int) bool { return rel.Files[i].Name < rel.Files[j].Name })
   252  		for _, f := range rel.Files {
   253  			if f.Log.Status == "" {
   254  				f.Log.Printf("FAIL: file not checked")
   255  			}
   256  			if f.Log.Status == FAIL {
   257  				rel.Log.Printf("FAIL: %s did not verify", f.Name)
   258  			}
   259  			if f.Log.Status == SKIP && rel.Log.Status == PASS {
   260  				rel.Log.Status = SKIP // be clear not completely verified
   261  			}
   262  		}
   263  		if rel.Log.Status == PASS {
   264  			rel.Log.Printf("PASS")
   265  		}
   266  		if rel.Log.Status == FAIL {
   267  			r.Log.Printf("FAIL: %s did not verify", rel.Version)
   268  			r.Log.Status = FAIL
   269  		}
   270  		if rel.Log.Status == SKIP && r.Log.Status == PASS {
   271  			r.Log.Status = SKIP // be clear not completely verified
   272  		}
   273  	}
   274  	if r.Log.Status == PASS {
   275  		r.Log.Printf("PASS")
   276  	}
   277  
   278  	return r
   279  }
   280  
   281  // defaultVersions returns the list of default versions to rebuild.
   282  // (See the package documentation for details about which ones.)
   283  func defaultVersions(releases []*DLRelease) []string {
   284  	var versions []string
   285  	seen := make(map[string]bool)
   286  	for _, r := range releases {
   287  		// Take the first unstable entry if there are no stable ones yet.
   288  		// That will be the latest release candidate.
   289  		// Otherwise skip; that will skip earlier release candidates
   290  		// and unstable older releases.
   291  		if !r.Stable {
   292  			if len(versions) == 0 {
   293  				versions = append(versions, r.Version)
   294  			}
   295  			continue
   296  		}
   297  
   298  		// Watch major versions go by. Take the first of each and stop after two.
   299  		major := r.Version
   300  		if strings.Count(major, ".") == 2 {
   301  			major = major[:strings.LastIndex(major, ".")]
   302  		}
   303  		if !seen[major] {
   304  			if major == "go1.20" {
   305  				// not reproducible
   306  				break
   307  			}
   308  			versions = append(versions, r.Version)
   309  			seen[major] = true
   310  			if len(seen) == 2 {
   311  				break
   312  			}
   313  		}
   314  	}
   315  	return versions
   316  }
   317  
   318  func (r *Report) ReproFile(rel *Release, file *File, src []byte) (err error) {
   319  	defer func() {
   320  		if err != nil {
   321  			file.Log.Printf("FAIL: %v", err)
   322  		}
   323  	}()
   324  
   325  	if file.dl == nil || file.dl.Kind != "archive" {
   326  		// Checked as a side effect of rebuilding a different file.
   327  		return nil
   328  	}
   329  
   330  	file.Log.Printf("start %s", file.Name)
   331  
   332  	goroot := filepath.Join(r.Work, fmt.Sprintf("repro-%s-%s-%s", rel.Version, file.GOOS, file.GOARCH))
   333  	defer os.RemoveAll(goroot)
   334  
   335  	if err := UnpackTarGz(goroot, src); err != nil {
   336  		return err
   337  	}
   338  	env := []string{"GOOS=" + file.GOOS, "GOARCH=" + file.GOARCH}
   339  	// For historical reasons, the linux-arm downloads are built
   340  	// with GOARM=6, even though the cross-compiled default is 7.
   341  	if strings.HasSuffix(file.Name, "-armv6l.tar.gz") || strings.HasSuffix(file.Name, ".linux-arm.zip") {
   342  		env = append(env, "GOARM=6")
   343  	}
   344  	if err := r.Build(&file.Log, goroot, rel.Version, env, []string{"-distpack"}); err != nil {
   345  		return err
   346  	}
   347  
   348  	distpack := filepath.Join(goroot, "pkg/distpack")
   349  	built, err := os.ReadDir(distpack)
   350  	if err != nil {
   351  		return err
   352  	}
   353  	for _, b := range built {
   354  		data, err := os.ReadFile(filepath.Join(distpack, b.Name()))
   355  		if err != nil {
   356  			return err
   357  		}
   358  
   359  		// Look up file from posted list.
   360  		// For historical reasons, the linux-arm downloads are named linux-armv6l.
   361  		// Other architectures are not renamed that way.
   362  		// Also, the module zips are not renamed that way, even on Linux.
   363  		name := b.Name()
   364  		if strings.HasPrefix(name, "go") && strings.HasSuffix(name, ".linux-arm.tar.gz") {
   365  			name = strings.TrimSuffix(name, "-arm.tar.gz") + "-armv6l.tar.gz"
   366  		}
   367  		bf := r.File(rel, name, file.GOOS, file.GOARCH)
   368  
   369  		pubData, ok := r.Download(bf)
   370  		if !ok {
   371  			continue
   372  		}
   373  
   374  		match := bytes.Equal(data, pubData)
   375  		if !match && file.GOOS == "darwin" {
   376  			if strings.HasSuffix(bf.Name, ".tar.gz") && DiffTarGz(&bf.Log, data, pubData, StripDarwinSig) ||
   377  				strings.HasSuffix(bf.Name, ".zip") && DiffZip(&bf.Log, data, pubData, StripDarwinSig) {
   378  				bf.Log.Printf("verified match after stripping signatures from executables")
   379  				match = true
   380  			}
   381  		}
   382  		if !match {
   383  			if strings.HasSuffix(bf.Name, ".tar.gz") {
   384  				DiffTarGz(&bf.Log, data, pubData, nil)
   385  			}
   386  			if strings.HasSuffix(bf.Name, ".zip") {
   387  				DiffZip(&bf.Log, data, pubData, nil)
   388  			}
   389  			bf.Log.Printf("FAIL: rebuilt SHA256 %s does not match public download SHA256 %s", SHA256(data), SHA256(pubData))
   390  			continue
   391  		}
   392  		bf.Log.Printf("PASS: rebuilt with %q", env)
   393  		if bf.dl != nil && bf.dl.Kind == "archive" {
   394  			if file.GOOS == "darwin" {
   395  				r.ReproDarwinPkg(rel, bf, pubData)
   396  			}
   397  			if file.GOOS == "windows" {
   398  				r.ReproWindowsMsi(rel, bf, pubData)
   399  			}
   400  		}
   401  	}
   402  	return nil
   403  }
   404  
   405  func (r *Report) ReproWindowsMsi(rel *Release, file *File, zip []byte) {
   406  	mf := r.File(rel, strings.TrimSuffix(file.Name, ".zip")+".msi", file.GOOS, file.GOARCH)
   407  	if mf.dl == nil {
   408  		mf.Log.Printf("FAIL: not found posted for download")
   409  		return
   410  	}
   411  	msi, ok := r.Download(mf)
   412  	if !ok {
   413  		return
   414  	}
   415  	ok, skip := DiffWindowsMsi(&mf.Log, zip, msi)
   416  	if ok {
   417  		mf.Log.Printf("PASS: verified content against posted zip")
   418  	} else if skip {
   419  		mf.Log.Printf("SKIP: msiextract not found")
   420  	}
   421  }
   422  
   423  func (r *Report) ReproDarwinPkg(rel *Release, file *File, tgz []byte) {
   424  	pf := r.File(rel, strings.TrimSuffix(file.Name, ".tar.gz")+".pkg", file.GOOS, file.GOARCH)
   425  	if pf.dl == nil {
   426  		pf.Log.Printf("FAIL: not found posted for download")
   427  		return
   428  	}
   429  	pkg, ok := r.Download(pf)
   430  	if !ok {
   431  		return
   432  	}
   433  	if DiffDarwinPkg(&pf.Log, tgz, pkg) {
   434  		pf.Log.Printf("PASS: verified content against posted tgz")
   435  	}
   436  }
   437  
   438  func (r *Report) Download(f *File) ([]byte, bool) {
   439  	url := "https://go.dev/dl/"
   440  	if strings.HasPrefix(f.Name, "v") {
   441  		url += "mod/golang.org/toolchain/@v/"
   442  	}
   443  	if f.cache {
   444  		f.mu.Lock()
   445  		defer f.mu.Unlock()
   446  		if f.data != nil {
   447  			return f.data, true
   448  		}
   449  	}
   450  	data, err := Get(&f.Log, url+f.Name)
   451  	if err != nil {
   452  		f.Log.Printf("FAIL: cannot download public copy")
   453  		return nil, false
   454  	}
   455  
   456  	sum := SHA256(data)
   457  	if f.dl != nil && f.dl.SHA256 != sum {
   458  		f.Log.Printf("FAIL: go.dev/dl-listed SHA256 %s does not match public download SHA256 %s", f.dl.SHA256, sum)
   459  		return nil, false
   460  	}
   461  	if f.cache {
   462  		f.data = data
   463  	}
   464  	return data, true
   465  }
   466  
   467  func (r *Report) Release(version string) *Release {
   468  	for _, rel := range r.Releases {
   469  		if rel.Version == version {
   470  			return rel
   471  		}
   472  	}
   473  
   474  	var dl *DLRelease
   475  	for _, dl = range r.dl {
   476  		if dl.Version == version {
   477  			rel := &Release{
   478  				Version: version,
   479  				dl:      dl,
   480  			}
   481  			rel.Log.Name = version
   482  			r.Releases = append(r.Releases, rel)
   483  			return rel
   484  		}
   485  	}
   486  	return nil
   487  }
   488  
   489  func (r *Report) File(rel *Release, name, goos, goarch string) *File {
   490  	rel.mu.Lock()
   491  	defer rel.mu.Unlock()
   492  
   493  	for _, f := range rel.Files {
   494  		if f.Name == name {
   495  			return f
   496  		}
   497  	}
   498  
   499  	f := &File{
   500  		Name:   name,
   501  		GOOS:   goos,
   502  		GOARCH: goarch,
   503  	}
   504  	f.Log.Name = name
   505  	rel.Files = append(rel.Files, f)
   506  	return f
   507  }