| // 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 fint |
| |
| import ( |
| "context" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| |
| "go.fuchsia.dev/fuchsia/tools/integration/fint/filetype" |
| "go.fuchsia.dev/fuchsia/tools/lib/jsonutil" |
| "go.fuchsia.dev/fuchsia/tools/lib/logger" |
| ) |
| |
| type gnAnalyzeInput struct { |
| // The files to use when determining affected targets. |
| Files []string `json:"files"` |
| |
| // A list of labels for targets that are needed to run the desired tests. |
| TestTargets []string `json:"test_targets"` |
| |
| // A list of labels for targets that should be rebuilt. |
| AdditionalCompileTargets []string `json:"additional_compile_targets"` |
| } |
| |
| type gnAnalyzeOutput struct { |
| // The status of the analyze call. |
| Status string `json:"status"` |
| |
| // The error, if present, associated with the analyze call. |
| Error string `json:"error"` |
| } |
| |
| // Valid values for the "status" field of the analyze output. |
| const ( |
| // The build graph is affected by the changed files. |
| buildGraphAffectedStatus = "Found dependency" |
| |
| // The build graph is NOT affected by the changed files. |
| buildGraphNotAffectedStatus = "No dependency" |
| |
| // GN can't determine whether the build graph is affected by the changed |
| // files, and it conservatively considers all targets to be affected. |
| unknownAffectedStatus = "Found dependency (all)" |
| ) |
| |
| // shouldBuild runs `gn analyze` on the given files to determine |
| // whether they are part of the build graph. It returns a boolean indicating |
| // whether changes to those files affect the build graph, and hence whether we |
| // need to do a full build to test those changes. |
| func shouldBuild( |
| ctx context.Context, |
| runner subprocessRunner, |
| buildDir string, |
| checkoutDir string, |
| platform string, |
| changedFiles []string, |
| ) (bool, error) { |
| gnPath := thirdPartyPrebuilt(checkoutDir, platform, "gn") |
| if !canAnalyzeFiles(ctx, changedFiles) { |
| // To be safe, we should always build if we don't know how to analyze |
| // all the affected files yet. |
| return true, nil |
| } |
| |
| input := gnAnalyzeInput{ |
| // Special string "all" tells GN to check all targets. |
| AdditionalCompileTargets: []string{"all"}, |
| // TestTargets must be an empty rather than nil slice so it gets |
| // serialized to an empty JSON array instead of null. |
| TestTargets: []string{}, |
| Files: formatFilePaths(changedFiles), |
| } |
| |
| analyzeDir, err := ioutil.TempDir("", "gn-analyze") |
| if err != nil { |
| return false, err |
| } |
| defer os.RemoveAll(analyzeDir) |
| |
| inputPath := filepath.Join(analyzeDir, "input.json") |
| if err := jsonutil.WriteToFile(inputPath, input); err != nil { |
| return false, err |
| } |
| logger.Debugf(ctx, "gn analyze input: %+v", input) |
| |
| outputPath := filepath.Join(analyzeDir, "output.json") |
| |
| cmd := []string{gnPath, "analyze", buildDir, inputPath, outputPath, fmt.Sprintf("--root=%s", checkoutDir)} |
| if err := runner.Run(ctx, cmd, nil, os.Stderr); err != nil { |
| return false, err |
| } |
| |
| var analyzeOutput gnAnalyzeOutput |
| if err := jsonutil.ReadFromFile(outputPath, &analyzeOutput); err != nil { |
| return false, fmt.Errorf("failed to read gn analyze output: %w", err) |
| } |
| logger.Debugf(ctx, "gn analyze output: %+v", analyzeOutput) |
| |
| if analyzeOutput.Error != "" { |
| return false, fmt.Errorf("gn analyze error: %q", analyzeOutput.Error) |
| } |
| |
| switch analyzeOutput.Status { |
| case buildGraphAffectedStatus, unknownAffectedStatus: |
| return true, nil |
| case buildGraphNotAffectedStatus: |
| return false, nil |
| default: |
| return false, fmt.Errorf("gn analyze produced unrecognized status: %s", analyzeOutput.Status) |
| } |
| } |
| |
| // formatFilePaths converts a series of file paths relative to the checkout root |
| // into the format expected by `gn analyze`. |
| func formatFilePaths(paths []string) []string { |
| formatted := []string{} // Empty, not nil, so it serializes to []. |
| for _, path := range paths { |
| formatted = append(formatted, "//"+path) |
| } |
| return formatted |
| } |
| |
| func canAnalyzeFiles(ctx context.Context, changedFiles []string) bool { |
| for _, path := range changedFiles { |
| ft := filetype.TypeForFile(path) |
| if !containsFileType(filetype.KnownFileTypes(), ft) { |
| logger.Debugf(ctx, "Build graph analysis is not supported for file %q", path) |
| return false |
| } |
| } |
| return true |
| } |
| |
| func containsFileType(collection []filetype.FileType, target filetype.FileType) bool { |
| for _, ft := range collection { |
| if ft == target { |
| return true |
| } |
| } |
| return false |
| } |