blob: d8295d1562caa30159154685353f5d7e9a64538c [file] [log] [blame]
// Copyright 2021 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 (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
)
type Gn struct {
gnPath string
outDir string
re *regexp.Regexp
}
// NewGn returns a GN object that is used to interface with the external GN tool. It can be used to
// discover the dependendcies of a GN target. The path to the external binary is taken from the
// command line argument (--gn_path). NewGn will return an error if gnPath is not a valid
// executable, or if --build_dir does not exist.
func NewGn(gnPath, buildDir string) (*Gn, error) {
gn := &Gn{
// Many rust_crate projects have a suffix in the label name that doesn't map to a directory.
// We use a regular expression to strip that part of the label text away.
// We store the regexp in this GN struct so we don't have to recompile the regex on each loop.
re: regexp.MustCompile(`-v\d_\d+_\d+`),
}
path, err := exec.LookPath(gnPath)
if err != nil {
return nil, err
}
if _, err := os.Stat(buildDir); os.IsNotExist(err) {
return nil, fmt.Errorf("out directory does not exist: %s", buildDir)
}
gn.gnPath = path
gn.outDir = buildDir
return gn, nil
}
// Return the dependencies of the given GN target. Calls out to the external GN executable.
// Saves the results to a file specified by gnFilterFile.
func (gn *Gn) Dependencies(ctx context.Context, gnFilterFile string, target string) error {
args := []string{
"desc",
gn.outDir,
target,
"deps",
"--all",
"--format=json",
}
cmd := exec.CommandContext(ctx, gn.gnPath, args...)
var output bytes.Buffer
cmd.Stdout = &output
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
result := output.String()
result = strings.TrimSpace(result)
var content interface{}
if err = json.Unmarshal([]byte(result), &content); err != nil {
return fmt.Errorf("Failed to unmarshal `gn desc` output file [%v]: %v\n", content, err)
}
return gn.unpack(content, gnFilterFile)
}
// Return the dependencies of the given GN workspace. Calls out to external GN executable.
// Saves the results to a file specified by gnFilterFile.
func (gn *Gn) Gen(ctx context.Context, gnFilterFile string) error {
projectFile := filepath.Join(gn.outDir, "project.json")
args := []string{
"gen",
gn.outDir,
"--all",
"--ide=json",
}
cmd := exec.CommandContext(ctx, gn.gnPath, args...)
var output bytes.Buffer
cmd.Stdout = &output
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
// Read in the projects.json file.
//
// This file can be really large (554MB on my machine), so we may
// need to investigate streaming this data if it becomes a problem.
b, err := ioutil.ReadFile(projectFile)
if err != nil {
return fmt.Errorf("Failed to read project.json file [%v]: %v\n", projectFile, err)
}
var content interface{}
if err = json.Unmarshal(b, &content); err != nil {
return fmt.Errorf("Failed to unmarshal project.json file [%v]: %v\n", projectFile, err)
}
return gn.unpack(content, gnFilterFile)
}
// Converts a GN label string (such as those returned by Dependencies) and strips any target names
// and toolchains, thereby returning the directory of the label.
func (gn *Gn) labelToDirectory(label string) []string {
results := make([]string, 0)
results = append(results, label)
// Rust crate dependencies are all linked into the build system
// using targets that are defined in the "rust_crates/BUILD.gn" file.
// We want to add the actual rust_crate subdirectory as a dependency,
// but there is no easy way to determine that from the build target name.
//
// This adds all possible directories to the list. There is no harm if
// a given directory doesn't actually exist -- check-licenses will ignore those entries.
if strings.Contains(label, "rust_crates") {
results = append(results, strings.ReplaceAll(label, ":", "/vendor/"))
results = append(results, strings.ReplaceAll(label, ":", "/ask2patch/"))
results = append(results, strings.ReplaceAll(label, ":", "/compat/"))
results = append(results, strings.ReplaceAll(label, ":", "/empty/"))
results = append(results, strings.ReplaceAll(label, ":", "/forks/"))
results = append(results, strings.ReplaceAll(label, ":", "/mirrors/"))
results = append(results, strings.ReplaceAll(label, ":", "/src/"))
results = append(results, strings.ReplaceAll(label, ":", "/vendor/"))
}
// If this target isn't a rust crate target, we still want to retrieve the relevant directory,
// not the target name in that directory.
// If a colon exists in this string, delete it and everything after it.
for i := range results {
results[i] = strings.Split(results[i], ":")[0]
}
// Same goes for toolchain definitions.
// If a parenthesis exists in this string, delete it and everything after it.
for i := range results {
results[i] = strings.Split(results[i], "(")[0]
}
// Many rust crate libraries have a version string in their target name,
// but no version string in their folder path. If we see this specific
// version string pattern, remove it from the string.
for i := range results {
results[i] = gn.re.ReplaceAllString(results[i], "")
}
return results
}
// The output of a "gn" command is a large json file.
// We want to retrieve any and all paths from that file, so we recursively
// look at each key / value / list item etc and add it to a string slice.
// Finally, we write that slice to the provided filepath.
func (gn *Gn) unpack(content interface{}, filename string) error {
var recurse func(interface{}) []string
recurse = func(content interface{}) []string {
results := make([]string, 0)
mapContent, ok := content.(map[string]interface{})
if ok {
for k, v := range mapContent {
results = append(results, k)
results = append(results, recurse(v)...)
}
}
listContent, ok := content.([]interface{})
if ok {
for _, v := range listContent {
results = append(results, recurse(v)...)
}
}
stringContent, ok := content.(string)
if ok {
results = append(results, gn.labelToDirectory(stringContent)...)
}
return results
}
results := recurse(content)
// Dedup the entries in the string slice.
set := make(map[string]bool, 0)
for _, s := range results {
set[s] = true
}
// Sort the results, so the outputs are deterministic.
results = make([]string, 0)
for k := range set {
results = append(results, k)
}
sort.Strings(results)
// Save the results to the provided filepath.
jsonContent, err := json.MarshalIndent(results, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(filename, jsonContent, 0644)
if err != nil {
return err
}
return nil
}