| // Copyright 2022 Google LLC |
| // |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package exporter |
| |
| import ( |
| "bytes" |
| "fmt" |
| "path/filepath" |
| "regexp" |
| "sort" |
| "strings" |
| |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/util" |
| "go.skia.org/skia/bazel/exporter/build_proto/build" |
| "go.skia.org/skia/bazel/exporter/interfaces" |
| "google.golang.org/protobuf/proto" |
| ) |
| |
| // The contents (or partial contents) of a GNI file. |
| type gniFileContents struct { |
| hasSrcs bool // Has at least one file in $_src/ dir? |
| hasIncludes bool // Has at least one file in $_include/ dir? |
| hasModules bool // Has at least one file in $_module/ dir? |
| bazelFiles map[string]bool // Set of Bazel files generating GNI contents. |
| data []byte // The file contents to be written. |
| } |
| |
| // GNIFileListExportDesc contains a description of the data that |
| // will comprise a GN file list variable when written to a *.gni file. |
| type GNIFileListExportDesc struct { |
| // The file list variable name to use in the exported *.gni file. |
| // In the *.gni file this will look like: |
| // var_name = [ ... ] |
| Var string |
| // The Bazel rule name(s) to export into the file list. |
| Rules []string |
| } |
| |
| // GNIExportDesc defines a GNI file to be exported, the rules to be |
| // exported, and the file list variable names in which to list the |
| // rule files. |
| type GNIExportDesc struct { |
| GNI string // The export destination *.gni file path (relative to workspace). |
| Vars []GNIFileListExportDesc // List of GNI file list variable rules. |
| } |
| |
| // GNIExporterParams contains the construction parameters when |
| // creating a new GNIExporter via NewGNIExporter(). |
| type GNIExporterParams struct { |
| WorkspaceDir string // The Bazel workspace directory path. |
| ExportDescs []GNIExportDesc // The Bazel rules to export. |
| } |
| |
| // GNIExporter is an object responsible for exporting rules defined in a |
| // Bazel workspace to file lists in GNI format (GN's *.gni files). This exporter |
| // is tightly coupled to the Skia Bazel rules and GNI configuration. |
| type GNIExporter struct { |
| workspaceDir string // The Bazel workspace path. |
| fs interfaces.FileSystem // For filesystem interactions. |
| exportGNIDescs []GNIExportDesc // The rules to export. |
| } |
| |
| // Skia source files which are deprecated. These are omitted from |
| // *.gni files during export because the skia.h file is a generated |
| // file and it cannot include deprecated files without breaking |
| // clients that include it. |
| var deprecatedFiles = []string{ |
| "include/core/SkDrawLooper.h", |
| "include/effects/SkBlurDrawLooper.h", |
| "include/effects/SkLayerDrawLooper.h", |
| } |
| |
| // The footer written to gn/core.gni. |
| const coreGNIFooter = `skia_core_sources += skia_pathops_sources |
| skia_core_sources += skia_skpicture_sources |
| |
| skia_core_public += skia_pathops_public |
| skia_core_public += skia_skpicture_public |
| # TODO(kjlubick) Move this into Chromium's BUILD.gn file. |
| skia_core_public += skia_discardable_memory_chromium |
| ` |
| |
| // The footer written to gn/sksl_tests.gni. |
| const skslTestsFooter = `sksl_glsl_tests_sources = |
| sksl_error_tests + sksl_glsl_tests + sksl_inliner_tests + |
| sksl_folding_tests + sksl_shared_tests + |
| sksl_inverse_hyperbolic_intrinsics_tests |
| |
| sksl_glsl_settings_tests_sources = sksl_blend_tests + sksl_settings_tests |
| |
| sksl_metal_tests_sources = |
| sksl_metal_tests + sksl_blend_tests + sksl_shared_tests + |
| sksl_inverse_hyperbolic_intrinsics_tests |
| |
| sksl_hlsl_tests_sources = sksl_blend_tests + sksl_shared_tests |
| |
| sksl_wgsl_tests_sources = sksl_wgsl_tests |
| |
| sksl_spirv_tests_sources = |
| sksl_blend_tests + sksl_shared_tests + |
| sksl_inverse_hyperbolic_intrinsics_tests + sksl_spirv_tests |
| |
| sksl_skrp_tests_sources = sksl_folding_tests + sksl_rte_tests + sksl_shared_tests |
| |
| sksl_skvm_tests_sources = sksl_rte_tests + sksl_rte_error_tests |
| |
| sksl_stage_tests_sources = sksl_rte_tests |
| |
| sksl_minify_tests_sources = sksl_rte_tests + sksl_folding_tests` |
| |
| // The footer written to modules/skshaper/skshaper.gni. |
| const skshaperFooter = ` |
| declare_args() { |
| skia_enable_skshaper = true |
| } |
| declare_args() { |
| skia_enable_skshaper_tests = skia_enable_skshaper |
| }` |
| |
| // The footer written to gn/gpu.gni. |
| const gpuGNIFooter = ` |
| # TODO(kjlubick) Update clients to use the individual targets |
| # instead of the monolithic ones. |
| skia_gpu_sources = skia_gpu_public + skia_gpu_private |
| skia_gl_gpu_sources = skia_gpu_gl_public + skia_gpu_gl_private + skia_gpu_chromium_public |
| skia_vk_sources = skia_gpu_vk_public + skia_gpu_vk_private + |
| skia_gpu_vk_chromium_public + skia_gpu_vk_chromium_private |
| skia_metal_sources = skia_gpu_metal_public + skia_gpu_metal_private + skia_gpu_metal_cpp |
| skia_dawn_sources = skia_gpu_dawn_public + skia_gpu_dawn_private |
| ` |
| |
| // The footer written to gn/utils.gni. |
| const utilsGNIFooter = ` |
| # TODO(kjlubick) Update pdfium to use the individual target |
| # instead of the monolithic ones. |
| skia_utils_sources = skia_utils_private + skia_utils_chromium |
| ` |
| |
| // Map of GNI file names to footer text to be appended to the end of the file. |
| var footerMap = map[string]string{ |
| "gn/core.gni": coreGNIFooter, |
| "gn/gpu.gni": gpuGNIFooter, |
| "gn/sksl_tests.gni": skslTestsFooter, |
| "gn/utils.gni": utilsGNIFooter, |
| "modules/skshaper/skshaper.gni": skshaperFooter, |
| } |
| |
| // Match variable definition of a list in a *.gni file. For example: |
| // |
| // foo = [] |
| // |
| // will match "foo" |
| var gniVariableDefReg = regexp.MustCompile(`^(\w+)\s?=\s?\[`) |
| |
| // NewGNIExporter creates an exporter that will export to GN's (*.gni) files. |
| func NewGNIExporter(params GNIExporterParams, filesystem interfaces.FileSystem) *GNIExporter { |
| e := &GNIExporter{ |
| workspaceDir: params.WorkspaceDir, |
| fs: filesystem, |
| exportGNIDescs: params.ExportDescs, |
| } |
| return e |
| } |
| |
| func makeGniFileContents() gniFileContents { |
| return gniFileContents{ |
| bazelFiles: make(map[string]bool), |
| } |
| } |
| |
| // Given a Bazel rule name find that rule from within the |
| // query results. Returns nil if the given rule is not present. |
| func findQueryResultRule(qr *build.QueryResult, name string) *build.Rule { |
| for _, target := range qr.GetTarget() { |
| r := target.GetRule() |
| if r.GetName() == name { |
| return r |
| } |
| } |
| return nil |
| } |
| |
| // Given a relative path to a file return the relative path to the |
| // top directory (in our case the workspace). For example: |
| // |
| // getPathToTopDir("path/to/file.h") -> "../.." |
| // |
| // The paths are to be delimited by forward slashes ('/') - even on |
| // Windows. |
| func getPathToTopDir(path string) string { |
| if filepath.IsAbs(path) { |
| return "" |
| } |
| d, _ := filepath.Split(path) |
| if d == "" { |
| return "." |
| } |
| d = strings.TrimSuffix(d, "/") |
| items := strings.Split(d, "/") |
| var sb = strings.Builder{} |
| for i := 0; i < len(items); i++ { |
| if i > 0 { |
| sb.WriteString("/") |
| } |
| sb.WriteString("..") |
| } |
| return sb.String() |
| } |
| |
| // Retrieve all rule attributes which are internal file targets. |
| func getRuleFiles(r *build.Rule, attrName string) ([]string, error) { |
| items, err := getRuleStringArrayAttribute(r, attrName) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| |
| var files []string |
| for _, item := range items { |
| if !isExternalRule(item) && isFileTarget(item) { |
| files = append(files, item) |
| } |
| } |
| return files, nil |
| } |
| |
| // Convert a file path into a workspace relative path using variables to |
| // specify the base folder. The variables are one of $_src, $_include, or $_modules. |
| func makeRelativeFilePathForGNI(path string) (string, error) { |
| if strings.HasPrefix(path, "src/") { |
| return "$_src/" + strings.TrimPrefix(path, "src/"), nil |
| } |
| if strings.HasPrefix(path, "include/") { |
| return "$_include/" + strings.TrimPrefix(path, "include/"), nil |
| } |
| if strings.HasPrefix(path, "modules/") { |
| return "$_modules/" + strings.TrimPrefix(path, "modules/"), nil |
| } |
| // These sksl tests are purposely listed as a relative path underneath resources/sksl because |
| // that relative path is re-used by the GN logic to put stuff under //tests/sksl as well. |
| if strings.HasPrefix(path, "resources/sksl/") { |
| return strings.TrimPrefix(path, "resources/sksl/"), nil |
| } |
| |
| return "", skerr.Fmt("can't find path for %q\n", path) |
| } |
| |
| // Convert a slice of workspace relative paths into a new slice containing |
| // GNI variables ($_src, $_include, etc.). *All* paths in the supplied |
| // slice must be a supported top-level directory. |
| func addGNIVariablesToWorkspacePaths(paths []string) ([]string, error) { |
| vars := make([]string, 0, len(paths)) |
| for _, path := range paths { |
| withVar, err := makeRelativeFilePathForGNI(path) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| vars = append(vars, withVar) |
| } |
| return vars, nil |
| } |
| |
| // Is the file path a C++ header? |
| func isHeaderFile(path string) bool { |
| ext := strings.ToLower(filepath.Ext(path)) |
| return ext == ".h" || ext == ".hpp" |
| } |
| |
| // Does the list of file paths contain only header files? |
| func fileListContainsOnlyCppHeaderFiles(files []string) bool { |
| for _, f := range files { |
| if !isHeaderFile(f) { |
| return false |
| } |
| } |
| return len(files) > 0 // Empty list is false, else all are headers. |
| } |
| |
| // Write the *.gni file header. |
| func writeGNFileHeader(writer interfaces.Writer, gniFile *gniFileContents, pathToWorkspace string) { |
| fmt.Fprintln(writer, "# DO NOT EDIT: This is a generated file.") |
| fmt.Fprintln(writer, "# See //bazel/exporter_tool/README.md for more information.") |
| |
| fmt.Fprintln(writer, "#") |
| if len(gniFile.bazelFiles) > 1 { |
| keys := make([]string, 0, len(gniFile.bazelFiles)) |
| fmt.Fprintln(writer, "# The sources of truth are:") |
| for bazelPath, _ := range gniFile.bazelFiles { |
| keys = append(keys, bazelPath) |
| } |
| sort.Strings(keys) |
| for _, wsPath := range keys { |
| fmt.Fprintf(writer, "# //%s\n", wsPath) |
| } |
| } else { |
| for bazelPath, _ := range gniFile.bazelFiles { |
| fmt.Fprintf(writer, "# The source of truth is //%s\n", bazelPath) |
| } |
| } |
| |
| writer.WriteString("\n") |
| fmt.Fprintln(writer, "# To update this file, run make -C bazel generate_gni") |
| |
| writer.WriteString("\n") |
| if gniFile.hasSrcs { |
| fmt.Fprintf(writer, "_src = get_path_info(\"%s/src\", \"abspath\")\n", pathToWorkspace) |
| } |
| if gniFile.hasIncludes { |
| fmt.Fprintf(writer, "_include = get_path_info(\"%s/include\", \"abspath\")\n", pathToWorkspace) |
| } |
| if gniFile.hasModules { |
| fmt.Fprintf(writer, "_modules = get_path_info(\"%s/modules\", \"abspath\")\n", pathToWorkspace) |
| } |
| } |
| |
| // removeDuplicates returns the list of files after it has been sorted and |
| // all duplicate values have been removed. |
| func removeDuplicates(files []string) []string { |
| if len(files) <= 1 { |
| return files |
| } |
| sort.Strings(files) |
| rv := make([]string, 0, len(files)) |
| rv = append(rv, files[0]) |
| for _, f := range files { |
| if rv[len(rv)-1] != f { |
| rv = append(rv, f) |
| } |
| } |
| return rv |
| } |
| |
| // Retrieve all sources ("srcs" attribute) and headers ("hdrs" attribute) |
| // and return as a single slice of target names. Slice entries will be |
| // something like: |
| // |
| // "//src/core/file.cpp". |
| func getSrcsAndHdrs(r *build.Rule) ([]string, error) { |
| srcs, err := getRuleFiles(r, "srcs") |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| |
| hdrs, err := getRuleFiles(r, "hdrs") |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| return append(srcs, hdrs...), nil |
| } |
| |
| // Convert a slice of file path targets to workspace relative file paths. |
| // i.e. convert each element like: |
| // |
| // "//src/core/file.cpp" |
| // |
| // into: |
| // |
| // "src/core/file.cpp" |
| func convertTargetsToFilePaths(targets []string) ([]string, error) { |
| paths := make([]string, 0, len(targets)) |
| for _, target := range targets { |
| path, err := getFilePathFromFileTarget(target) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| paths = append(paths, path) |
| } |
| return paths, nil |
| } |
| |
| // Is the source file deprecated? i.e. should the file be exported to projects |
| // generated by this package? |
| func isSourceFileDeprecated(workspacePath string) bool { |
| return util.In(workspacePath, deprecatedFiles) |
| } |
| |
| // Filter all deprecated files from the |files| slice, returning a new slice |
| // containing no deprecated files. All paths in |files| must be workspace-relative |
| // paths. |
| func filterDeprecatedFiles(files []string) []string { |
| filtered := make([]string, 0, len(files)) |
| for _, path := range files { |
| if !isSourceFileDeprecated(path) { |
| filtered = append(filtered, path) |
| } |
| } |
| return filtered |
| } |
| |
| // Return the top-level component (directory or file) of a relative file path. |
| // The paths are assumed to be delimited by forward slash (/) characters (even on Windows). |
| // An empty string is returned if no top level folder can be found. |
| // |
| // Example: |
| // |
| // "foo/bar/baz.txt" returns "foo" |
| func extractTopLevelFolder(path string) string { |
| parts := strings.Split(path, "/") |
| if len(parts) > 0 { |
| return parts[0] |
| } |
| return "" |
| } |
| |
| // Extract the name of a variable assignment from a line of text from a GNI file. |
| // So, a line like: |
| // |
| // "foo = [...]" |
| // |
| // will return: |
| // |
| // "foo" |
| func getGNILineVariable(line string) string { |
| if matches := gniVariableDefReg.FindStringSubmatch(line); matches != nil { |
| return matches[1] |
| } |
| return "" |
| } |
| |
| // Given a workspace relative path return an absolute path. |
| func (e *GNIExporter) workspaceToAbsPath(wsPath string) string { |
| if filepath.IsAbs(wsPath) { |
| panic("filepath already absolute") |
| } |
| return filepath.Join(e.workspaceDir, wsPath) |
| } |
| |
| // Given an absolute path return a workspace relative path. |
| func (e *GNIExporter) absToWorkspacePath(absPath string) (string, error) { |
| if !filepath.IsAbs(absPath) { |
| return "", skerr.Fmt(`"%s" is not an absolute path`, absPath) |
| } |
| if absPath == e.workspaceDir { |
| return "", nil |
| } |
| wsDir := e.workspaceDir + "/" |
| if !strings.HasPrefix(absPath, wsDir) { |
| return "", skerr.Fmt(`"%s" is not in the workspace "%s"`, absPath, wsDir) |
| } |
| return strings.TrimPrefix(absPath, wsDir), nil |
| } |
| |
| // Merge the another file contents object into this one. |
| func (c *gniFileContents) merge(other gniFileContents) { |
| if other.hasIncludes { |
| c.hasIncludes = true |
| } |
| if other.hasModules { |
| c.hasModules = true |
| } |
| if other.hasSrcs { |
| c.hasSrcs = true |
| } |
| for path, _ := range other.bazelFiles { |
| c.bazelFiles[path] = true |
| } |
| c.data = append(c.data, other.data...) |
| } |
| |
| // Convert all rules that go into a GNI file list. |
| func (e *GNIExporter) convertGNIFileList(desc GNIFileListExportDesc, qr *build.QueryResult) (gniFileContents, error) { |
| var rules []string |
| fileContents := makeGniFileContents() |
| var targets []string |
| for _, ruleName := range desc.Rules { |
| r := findQueryResultRule(qr, ruleName) |
| if r == nil { |
| return gniFileContents{}, skerr.Fmt("Cannot find rule %s", ruleName) |
| } |
| absBazelPath, _, _, err := parseLocation(*r.Location) |
| if err != nil { |
| return gniFileContents{}, skerr.Wrap(err) |
| } |
| wsBazelpath, err := e.absToWorkspacePath(absBazelPath) |
| if err != nil { |
| return gniFileContents{}, skerr.Wrap(err) |
| } |
| fileContents.bazelFiles[wsBazelpath] = true |
| t, err := getSrcsAndHdrs(r) |
| if err != nil { |
| return gniFileContents{}, skerr.Wrap(err) |
| } |
| if len(t) == 0 { |
| return gniFileContents{}, skerr.Fmt("No files to export in rule %s", ruleName) |
| } |
| targets = append(targets, t...) |
| rules = append(rules, ruleName) |
| } |
| |
| files, err := convertTargetsToFilePaths(targets) |
| if err != nil { |
| return gniFileContents{}, skerr.Wrap(err) |
| } |
| |
| files = filterDeprecatedFiles(files) |
| |
| files, err = addGNIVariablesToWorkspacePaths(files) |
| if err != nil { |
| return gniFileContents{}, skerr.Wrap(err) |
| } |
| |
| files = removeDuplicates(files) |
| |
| for i := range files { |
| if strings.HasPrefix(files[i], "$_src/") { |
| fileContents.hasSrcs = true |
| } else if strings.HasPrefix(files[i], "$_include/") { |
| fileContents.hasIncludes = true |
| } else if strings.HasPrefix(files[i], "$_modules/") { |
| fileContents.hasModules = true |
| } |
| } |
| |
| var contents bytes.Buffer |
| |
| if len(rules) > 1 { |
| fmt.Fprintln(&contents, "# List generated by Bazel rules:") |
| for _, bazelFile := range rules { |
| fmt.Fprintf(&contents, "# %s\n", bazelFile) |
| } |
| } else { |
| fmt.Fprintf(&contents, "# Generated by Bazel rule %s\n", rules[0]) |
| } |
| fmt.Fprintf(&contents, "%s = [\n", desc.Var) |
| |
| for _, target := range files { |
| fmt.Fprintf(&contents, " %q,\n", target) |
| } |
| fmt.Fprintln(&contents, "]") |
| fmt.Fprintln(&contents) |
| fileContents.data = contents.Bytes() |
| |
| return fileContents, nil |
| } |
| |
| // Export all Bazel rules to a single *.gni file. |
| func (e *GNIExporter) exportGNIFile(gniExportDesc GNIExportDesc, qr *build.QueryResult) error { |
| // Keep the contents of each file list in memory before writing to disk. |
| // This is done so that we know what variables to define for each of the |
| // file lists. i.e. $_src, $_include, etc. |
| gniFileContents := makeGniFileContents() |
| for _, varDesc := range gniExportDesc.Vars { |
| fileListContents, err := e.convertGNIFileList(varDesc, qr) |
| if err != nil { |
| return skerr.Wrap(err) |
| } |
| gniFileContents.merge(fileListContents) |
| } |
| |
| writer, err := e.fs.OpenFile(e.workspaceToAbsPath(gniExportDesc.GNI)) |
| if err != nil { |
| return skerr.Wrap(err) |
| } |
| |
| pathToWorkspace := getPathToTopDir(gniExportDesc.GNI) |
| writeGNFileHeader(writer, &gniFileContents, pathToWorkspace) |
| writer.WriteString("\n") |
| |
| _, err = writer.Write(gniFileContents.data) |
| if err != nil { |
| return skerr.Wrap(err) |
| } |
| |
| for gniPath, footer := range footerMap { |
| if gniExportDesc.GNI == gniPath { |
| fmt.Fprintln(writer, footer) |
| break |
| } |
| } |
| |
| return nil |
| } |
| |
| // Export the contents of a Bazel query response to one or more GNI |
| // files. |
| // |
| // The Bazel data to export, and the destination GNI files are defined |
| // by the configuration data supplied to NewGNIExporter(). |
| func (e *GNIExporter) Export(qcmd interfaces.QueryCommand) error { |
| in, err := qcmd.Read() |
| if err != nil { |
| return skerr.Wrapf(err, "error reading bazel cquery data") |
| } |
| qr := &build.QueryResult{} |
| if err := proto.Unmarshal(in, qr); err != nil { |
| return skerr.Wrapf(err, "failed to unmarshal cquery result") |
| } |
| for _, desc := range e.exportGNIDescs { |
| err = e.exportGNIFile(desc, qr) |
| if err != nil { |
| return skerr.Wrap(err) |
| } |
| } |
| return nil |
| } |
| |
| // Make sure GNIExporter fulfills the Exporter interface. |
| var _ interfaces.Exporter = (*GNIExporter)(nil) |