blob: 4c2f407967318450ce47e9ad68bad200baa00386 [file] [log] [blame]
// 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)
}
}