platform_build_soong/cmd/symbols_map/symbols_map.go
Colin Cross 36f55aabcd Add a symbols_map tool for extracting identifiers from elf and r8 files
Add a symbols_map tool that can extract an identifiying hash from
and elf file or an r8 dictionary.  The tool writes the hash to a
textproto, and also supports a merge mode to combine textprotos into
a output file for inclusion in the build artifacts.

Bug: 218888599
Test: m dist
Test: symbols_map_test.go
Change-Id: Icd3ed6e5510e058c92d97c78759e7a4cfcdbb6ca
2022-04-04 15:53:38 -07:00

202 lines
6.3 KiB
Go

// Copyright 2022 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
"android/soong/cmd/symbols_map/symbols_map_proto"
"android/soong/response"
"github.com/google/blueprint/pathtools"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
)
// This tool is used to extract a hash from an elf file or an r8 dictionary and store it as a
// textproto, or to merge multiple textprotos together.
func main() {
var expandedArgs []string
for _, arg := range os.Args[1:] {
if strings.HasPrefix(arg, "@") {
f, err := os.Open(strings.TrimPrefix(arg, "@"))
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
respArgs, err := response.ReadRspFile(f)
f.Close()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
expandedArgs = append(expandedArgs, respArgs...)
} else {
expandedArgs = append(expandedArgs, arg)
}
}
flags := flag.NewFlagSet("flags", flag.ExitOnError)
// Hide the flag package to prevent accidental references to flag instead of flags.
flag := struct{}{}
_ = flag
flags.Usage = func() {
fmt.Fprintf(flags.Output(), "Usage of %s:\n", os.Args[0])
fmt.Fprintf(flags.Output(), " %s -elf|-r8 <input file> [-write_if_changed] <output file>\n", os.Args[0])
fmt.Fprintf(flags.Output(), " %s -merge <output file> [-write_if_changed] [-ignore_missing_files] [-strip_prefix <prefix>] [<input file>...]\n", os.Args[0])
fmt.Fprintln(flags.Output())
flags.PrintDefaults()
}
elfFile := flags.String("elf", "", "extract identifier from an elf file")
r8File := flags.String("r8", "", "extract identifier from an r8 dictionary")
merge := flags.String("merge", "", "merge multiple identifier protos")
writeIfChanged := flags.Bool("write_if_changed", false, "only write output file if it is modified")
ignoreMissingFiles := flags.Bool("ignore_missing_files", false, "ignore missing input files in merge mode")
stripPrefix := flags.String("strip_prefix", "", "prefix to strip off of the location field in merge mode")
flags.Parse(expandedArgs)
if *merge != "" {
// If merge mode was requested perform the merge and exit early.
err := mergeProtos(*merge, flags.Args(), *stripPrefix, *writeIfChanged, *ignoreMissingFiles)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to merge protos: %s", err)
os.Exit(1)
}
os.Exit(0)
}
if *elfFile == "" && *r8File == "" {
fmt.Fprintf(os.Stderr, "-elf or -r8 argument is required\n")
flags.Usage()
os.Exit(1)
}
if *elfFile != "" && *r8File != "" {
fmt.Fprintf(os.Stderr, "only one of -elf or -r8 argument is allowed\n")
flags.Usage()
os.Exit(1)
}
if flags.NArg() != 1 {
flags.Usage()
os.Exit(1)
}
output := flags.Arg(0)
var identifier string
var location string
var typ symbols_map_proto.Mapping_Type
var err error
if *elfFile != "" {
typ = symbols_map_proto.Mapping_ELF
location = *elfFile
identifier, err = elfIdentifier(*elfFile, true)
if err != nil {
fmt.Fprintf(os.Stderr, "error reading elf identifier: %s\n", err)
os.Exit(1)
}
} else if *r8File != "" {
typ = symbols_map_proto.Mapping_R8
identifier, err = r8Identifier(*r8File)
location = *r8File
if err != nil {
fmt.Fprintf(os.Stderr, "error reading r8 identifier: %s\n", err)
os.Exit(1)
}
} else {
panic("shouldn't get here")
}
mapping := symbols_map_proto.Mapping{
Identifier: proto.String(identifier),
Location: proto.String(location),
Type: typ.Enum(),
}
err = writeTextProto(output, &mapping, *writeIfChanged)
if err != nil {
fmt.Fprintf(os.Stderr, "error writing output: %s\n", err)
os.Exit(1)
}
}
// writeTextProto writes a proto to an output file as a textproto, optionally leaving the file
// unmodified if it was already up to date.
func writeTextProto(output string, message proto.Message, writeIfChanged bool) error {
marshaller := prototext.MarshalOptions{Multiline: true}
data, err := marshaller.Marshal(message)
if err != nil {
return fmt.Errorf("error marshalling textproto: %w", err)
}
if writeIfChanged {
err = pathtools.WriteFileIfChanged(output, data, 0666)
} else {
err = ioutil.WriteFile(output, data, 0666)
}
if err != nil {
return fmt.Errorf("error writing to %s: %w\n", output, err)
}
return nil
}
// mergeProtos merges a list of textproto files containing Mapping messages into a single textproto
// containing a Mappings message.
func mergeProtos(output string, inputs []string, stripPrefix string, writeIfChanged bool, ignoreMissingFiles bool) error {
mappings := symbols_map_proto.Mappings{}
for _, input := range inputs {
mapping := symbols_map_proto.Mapping{}
data, err := ioutil.ReadFile(input)
if err != nil {
if ignoreMissingFiles && os.IsNotExist(err) {
// Merge mode is used on a list of files in the packaging directory. If multiple
// goals are included on the build command line, for example `dist` and `tests`,
// then the symbols packaging rule for `dist` can run while a dependency of `tests`
// is modifying the symbols packaging directory. That can result in a file that
// existed when the file list was generated being deleted as part of updating it,
// resulting in sporadic ENOENT errors. Ignore them if -ignore_missing_files
// was passed on the command line.
continue
}
return fmt.Errorf("failed to read %s: %w", input, err)
}
err = prototext.Unmarshal(data, &mapping)
if err != nil {
return fmt.Errorf("failed to parse textproto %s: %w", input, err)
}
if stripPrefix != "" && mapping.Location != nil {
mapping.Location = proto.String(strings.TrimPrefix(*mapping.Location, stripPrefix))
}
mappings.Mappings = append(mappings.Mappings, &mapping)
}
return writeTextProto(output, &mappings, writeIfChanged)
}