| // Copyright 2018 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. |
| |
| package main |
| |
| import ( |
| "crypto/rand" |
| "encoding/hex" |
| "flag" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| |
| "go.fuchsia.dev/fuchsia/tools/debug/elflib" |
| "go.fuchsia.dev/fuchsia/tools/lib/color" |
| "go.fuchsia.dev/fuchsia/tools/lib/logger" |
| ) |
| |
| type entry struct { |
| suffix string |
| file string |
| } |
| |
| type entryList []entry |
| |
| func (a *entryList) String() string { |
| return fmt.Sprintf("%v", []entry(*a)) |
| } |
| |
| func (a *entryList) Set(value string) error { |
| args := strings.SplitN(value, "=", 2) |
| if len(args) != 2 { |
| return fmt.Errorf("'%s' is not a valid entry. Must be in format <suffix>=<file>", value) |
| } |
| *a = append(*a, entry{args[0], args[1]}) |
| return nil |
| } |
| |
| var ( |
| buildIDDir string |
| stamp string |
| entries entryList |
| colors color.EnableColor |
| level logger.LogLevel |
| ) |
| |
| func init() { |
| colors = color.ColorAuto |
| level = logger.FatalLevel |
| |
| flag.StringVar(&buildIDDir, "build-id-dir", "", "path to .build-id dirctory") |
| flag.StringVar(&stamp, "stamp", "", "path to stamp file which acts as a stand in for the .build-id file") |
| flag.Var(&entries, "entry", "supply <suffix>=<file> to link <file> into .build-id with the given suffix") |
| flag.Var(&colors, "color", "use color in output, can be never, auto, always") |
| flag.Var(&level, "level", "output verbosity, can be fatal, error, warning, info, debug or trace") |
| } |
| |
| func getTmpFile(path string, name string) (string, error) { |
| out := make([]byte, 16) |
| if _, err := rand.Read(out); err != nil { |
| return "", nil |
| } |
| return filepath.Join(path, name+"-"+hex.EncodeToString(out)) + ".tmp", nil |
| } |
| |
| func atomicLink(from, to string) error { |
| // First make sure the directory already exists |
| if err := os.MkdirAll(filepath.Dir(to), 0700); err != nil { |
| return err |
| } |
| // Make a tmpFile in the same directory as 'from' |
| dir, file := filepath.Split(from) |
| tmpFile, err := getTmpFile(dir, file) |
| if err != nil { |
| return err |
| } |
| if err := os.Link(from, tmpFile); err != nil { |
| return err |
| } |
| if err := os.Rename(tmpFile, to); err != nil { |
| return err |
| } |
| // If "tmpFile" and "to" are already links to the same inode Rename does not remove tmpFile. |
| if err := os.Remove(tmpFile); !os.IsNotExist(err) { |
| return err |
| } |
| return nil |
| } |
| |
| func atomicWrite(file, fmtStr string, args ...interface{}) error { |
| dir, base := filepath.Split(file) |
| tmpFile, err := getTmpFile(dir, base) |
| if err != nil { |
| return err |
| } |
| f, err := os.Create(tmpFile) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| _, err = fmt.Fprintf(f, fmtStr, args...) |
| if err != nil { |
| return err |
| } |
| return os.Rename(tmpFile, file) |
| } |
| |
| func removeOldFile(newBuildID, suffix string) error { |
| data, err := os.ReadFile(stamp) |
| if err != nil { |
| if !os.IsNotExist(err) { |
| return err |
| } |
| return nil |
| } |
| oldBuildID := string(data) |
| // Nothing to be removed if build ID wasn't previously set. |
| if oldBuildID == "" { |
| return nil |
| } |
| // We don't want to remove what we just added! |
| if newBuildID == oldBuildID { |
| return nil |
| } |
| oldPath := filepath.Join(buildIDDir, oldBuildID[:2], oldBuildID[2:]) + suffix |
| // If the file has already been removed (perhaps by another process) then |
| // just keep going. |
| if err := os.Remove(oldPath); !os.IsNotExist(err) { |
| return err |
| } |
| return nil |
| } |
| |
| type entryInfo struct { |
| ref elflib.BinaryFileRef |
| suffix string |
| } |
| |
| func getEntriesInfo() ([]entryInfo, error) { |
| var outs []entryInfo |
| for _, entry := range entries { |
| f, err := os.Open(entry.file) |
| if err != nil { |
| return nil, fmt.Errorf("opening %s to read build ID: %v", entry.file, err) |
| } |
| defer f.Close() |
| buildIDs, err := elflib.GetBuildIDs(entry.file, f) |
| if err != nil { |
| return nil, fmt.Errorf("reading build ID from %s: %v", entry.file, err) |
| } |
| // Ignore entries where the binary doesn't have a build ID. |
| if len(buildIDs) == 0 { |
| continue |
| } |
| if len(buildIDs) != 1 { |
| return nil, fmt.Errorf("unexpected number of build IDs in %s. Expected 1 but found %v", entry.file, buildIDs) |
| } |
| if len(buildIDs[0]) < 2 { |
| return nil, fmt.Errorf("build ID (%s) is too short in %s", buildIDs[0], entry.file) |
| } |
| buildID := hex.EncodeToString(buildIDs[0]) |
| outs = append(outs, entryInfo{elflib.BinaryFileRef{BuildID: buildID, Filepath: entry.file}, entry.suffix}) |
| } |
| return outs, nil |
| } |
| |
| func main() { |
| l := logger.NewLogger(level, color.NewColor(colors), os.Stderr, os.Stderr, "") |
| // Parse flags and check for errors. |
| flag.Parse() |
| if buildIDDir == "" { |
| l.Fatalf("-build-id-dir is required.") |
| } |
| if stamp == "" { |
| l.Fatalf("-stamp file is required.") |
| } |
| if len(entries) == 0 { |
| l.Fatalf("Need at least one -entry arg") |
| } |
| // Get the build IDs |
| infos, err := getEntriesInfo() |
| if err != nil { |
| l.Fatalf("Parsing entries: %v", err) |
| } |
| var buildID string |
| if len(infos) != 0 { |
| buildID = infos[0].ref.BuildID |
| for _, info := range infos { |
| if buildID != info.ref.BuildID { |
| l.Fatalf("%s and %s do not have the same build ID", info.ref.Filepath, infos[0].ref.Filepath) |
| } |
| if err := info.ref.Verify(); err != nil { |
| l.Fatalf("Could not verify build ID of %s: %v", info.ref.Filepath, err) |
| } |
| } |
| // Now that we know all the build IDs are in order perform operations. |
| // Make sure to not output the stamp file until all of these operations are |
| // performed to ensure that this tool is re-run if it fails mid-run. |
| buildIDRunes := []rune(buildID) |
| buildIDPathPrefix := filepath.Join(buildIDDir, string(buildIDRunes[:2]), string(buildIDRunes[2:])) |
| for _, info := range infos { |
| buildIDPath := buildIDPathPrefix + info.suffix |
| if err = atomicLink(info.ref.Filepath, buildIDPath); err != nil { |
| l.Fatalf("atomically linking %s to %s: %v", info.ref.Filepath, buildIDPath, err) |
| } |
| if err = removeOldFile(buildID, info.suffix); err != nil { |
| l.Fatalf("removing old file referenced by %s: %v", stamp, err) |
| } |
| } |
| } |
| // Update the stamp last atomically to commit all the above operations. |
| if err = atomicWrite(stamp, buildID); err != nil { |
| l.Fatalf("emitting final stamp %s: %v", stamp, err) |
| } |
| } |