| // 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 |
| } |