blob: 51cff0dad8797da7e40483fa4d221e5dadbd6d9a [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Uploads binary debug symbols to Google Cloud Storage.
//
// Example Usage:
//
// $ upload_debug_symbols -j 20 -bucket bucket-name -upload-record /path/to/record /path/to/.build-id
package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"strings"
"sync"
"go.fuchsia.dev/tools/debug/elflib"
)
const (
usage = `upload_debug_symbols [flags] [paths..]
Uploads binary debug symbols to Google Cloud Storage.
`
// The default number of files to upload at once. The storage API returns 4xx errors
// when we spawn too many go routines at once, and we can't predict the number of
// symbol files we'll have to upload. 100 is chosen as a sensible default. This can be
// overriden on the command line.
defaultConccurrentUploadCount = 100
)
// Command line flags.
var (
// The GCS bucket to upload files to.
gcsBucket string
// GCS path to record of uploaded files.
uploadRecord string
// The maximum number of files to upload at once.
concurrentUploadCount int
)
func init() {
flag.Usage = func() {
fmt.Fprint(os.Stderr, usage)
flag.PrintDefaults()
os.Exit(1)
}
flag.StringVar(&gcsBucket, "bucket", "", "GCS bucket to upload symbols to")
flag.StringVar(&uploadRecord, "upload-record", "", "Path to write record of uploaded symbols")
flag.IntVar(&concurrentUploadCount, "j", defaultConccurrentUploadCount, "Number of concurrent threads to use to upload files")
}
func main() {
flag.Parse()
if flag.NArg() == 0 {
log.Fatal("expected at least one path to a .build-id directory")
}
if gcsBucket == "" {
log.Fatal("missing -bucket")
}
if err := execute(context.Background(), flag.Args()); err != nil {
log.Fatal(err)
}
}
func execute(ctx context.Context, paths []string) error {
bfrs, err := collectDebugSymbolFiles(paths)
if err != nil {
return fmt.Errorf("failed to collect symbol files: %v", err)
}
bfrs = filterInvalidDebugSymbolFiles(bfrs)
jobs, err := queueJobs(bfrs)
if err != nil {
return fmt.Errorf("failed to queue jobs: %v", err)
}
bkt, err := newGCSBucket(ctx, gcsBucket)
if err != nil {
return err
}
succeeded, uploadPaths := upload(ctx, bkt, jobs)
if !succeeded {
return errors.New("completed with errors")
}
if uploadRecord != "" {
if err = writeUploadRecord(uploadRecord, uploadPaths); err != nil {
return fmt.Errorf("failed to write record of uploaded symbols: %v", err)
}
log.Printf("wrote record of uploaded symbols to %s\n", uploadRecord)
}
return nil
}
// Returns filtered input of BinaryFileRefs, skipping files without .debug_info header or valid build ID.
func filterInvalidDebugSymbolFiles(bfrs []elflib.BinaryFileRef) []elflib.BinaryFileRef {
var filteredBfrs []elflib.BinaryFileRef
for _, bfr := range bfrs {
hasDebugInfo, err := bfr.HasDebugInfo()
if err != nil {
log.Printf("WARNING: cannot read file %s: %v, skipping\n", bfr.Filepath, err)
} else if !hasDebugInfo {
log.Printf("WARNING: file %s missing .debug_info section, skipping\n", bfr.Filepath)
} else if err := bfr.Verify(); err != nil {
log.Printf("WARNING: validation failed for %s: %v, skipping\n", bfr.Filepath, err)
} else {
filteredBfrs = append(filteredBfrs, bfr)
}
}
return filteredBfrs
}
// Creates BinaryFileRefs for all debug symbol files in the directories named in dirs.
func collectDebugSymbolFiles(dirs []string) ([]elflib.BinaryFileRef, error) {
var out []elflib.BinaryFileRef
for _, dir := range dirs {
refs, err := elflib.WalkBuildIDDir(dir)
if err != nil {
return nil, err
}
out = append(out, refs...)
}
return out, nil
}
// Returns a read-only channel of jobs to upload each file referenced in bfrs.
func queueJobs(bfrs []elflib.BinaryFileRef) (<-chan job, error) {
jobs := make(chan job, len(bfrs))
for _, bfr := range bfrs {
jobs <- newJob(bfr, gcsBucket)
}
close(jobs)
return jobs, nil
}
// Upload executes all of the jobs to upload files from the input channel. Returns true
// iff all uploads succeeded without error, and a record of all uploads as a string.
func upload(ctx context.Context, bkt *GCSBucket, jobs <-chan job) (bool, string) {
errs := make(chan error, concurrentUploadCount)
defer close(errs)
uploadPaths := make(chan string, concurrentUploadCount)
defer close(uploadPaths)
// Spawn workers to execute the uploads.
workerCount := concurrentUploadCount
var wg sync.WaitGroup
wg.Add(workerCount)
for i := 0; i < workerCount; i++ {
go worker(ctx, bkt, &wg, jobs, errs, uploadPaths)
}
// Let the caller know whether any errors were emitted.
succeeded := true
go func() {
for e := range errs {
succeeded = false
log.Printf("error: %v", e)
}
}()
// Receive from uploadPaths channel to build upload record.
var builder strings.Builder
go func() {
for uploadPath := range uploadPaths {
fmt.Fprintf(&builder, "%s\n", uploadPath)
}
}()
wg.Wait()
return succeeded, builder.String()
}
// worker processes all jobs on the input channel, emitting any errors on errs.
func worker(ctx context.Context, bkt *GCSBucket, wg *sync.WaitGroup, jobs <-chan job, errs chan<- error, uploadPaths chan<- string) {
defer wg.Done()
for job := range jobs {
log.Printf("executing %s", job.name)
err := job.execute(context.Background(), bkt)
if err != nil {
errs <- fmt.Errorf("job %s failed: %v", job.name, err)
} else {
uploadPaths <- job.gcsPath
}
}
}
// Write upload paths to local file.
func writeUploadRecord(uploadRecord string, uploadPaths string) error {
file, err := os.Create(uploadRecord)
if err != nil {
return err
}
defer file.Close()
_, err = io.WriteString(file, uploadPaths)
return err
}