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 }