xref: /aosp_15_r20/external/skia/bazel/exporter/gni_exporter.go (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1// Copyright 2022 Google LLC
2//
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5
6package exporter
7
8import (
9	"bytes"
10	"fmt"
11	"path/filepath"
12	"regexp"
13	"sort"
14	"strings"
15
16	"go.skia.org/infra/go/skerr"
17	"go.skia.org/skia/bazel/exporter/build_proto/build"
18	"go.skia.org/skia/bazel/exporter/interfaces"
19	"google.golang.org/protobuf/proto"
20)
21
22// The contents (or partial contents) of a GNI file.
23type gniFileContents struct {
24	hasExperimental bool            // Has at least one file in $_experimental/ dir?
25	hasIncludes     bool            // Has at least one file in $_include/ dir?
26	hasModules      bool            // Has at least one file in $_module/ dir?
27	hasSrcs         bool            // Has at least one file in $_src/ dir?
28	bazelFiles      map[string]bool // Set of Bazel files generating GNI contents.
29	data            []byte          // The file contents to be written.
30}
31
32// GNIFileListExportDesc contains a description of the data that
33// will comprise a GN file list variable when written to a *.gni file.
34type GNIFileListExportDesc struct {
35	// The file list variable name to use in the exported *.gni file.
36	// In the *.gni file this will look like:
37	//   var_name = [ ... ]
38	Var string
39	// The Bazel rule name(s) to export into the file list.
40	Rules []string
41}
42
43// GNIExportDesc defines a GNI file to be exported, the rules to be
44// exported, and the file list variable names in which to list the
45// rule files.
46type GNIExportDesc struct {
47	GNI  string                  // The export destination *.gni file path (relative to workspace).
48	Vars []GNIFileListExportDesc // List of GNI file list variable rules.
49}
50
51// GNIExporterParams contains the construction parameters when
52// creating a new GNIExporter via NewGNIExporter().
53type GNIExporterParams struct {
54	WorkspaceDir string          // The Bazel workspace directory path.
55	ExportDescs  []GNIExportDesc // The Bazel rules to export.
56}
57
58// GNIExporter is an object responsible for exporting rules defined in a
59// Bazel workspace to file lists in GNI format (GN's *.gni files). This exporter
60// is tightly coupled to the Skia Bazel rules and GNI configuration.
61type GNIExporter struct {
62	workspaceDir   string                // The Bazel workspace path.
63	fs             interfaces.FileSystem // For filesystem interactions.
64	exportGNIDescs []GNIExportDesc       // The rules to export.
65}
66
67// The footer written to gn/codec.gni.
68const codecGNIFooter = `
69skia_codec_rust_png_ffi_crate_root = "$_experimental/rust_png/ffi/FFI.rs"
70`
71
72// The footer written to gn/core.gni.
73const coreGNIFooter = `skia_core_sources += skia_pathops_sources
74
75skia_core_public += skia_pathops_public
76`
77
78// The footer written to gn/sksl_tests.gni.
79const skslTestsFooter = `sksl_glsl_tests_sources =
80    sksl_error_tests + sksl_glsl_tests + sksl_inliner_tests +
81    sksl_folding_tests + sksl_shared_tests
82
83sksl_glsl_settings_tests_sources = sksl_blend_tests + sksl_settings_tests
84
85sksl_metal_tests_sources =
86    sksl_blend_tests + sksl_compute_tests + sksl_metal_tests + sksl_shared_tests
87
88sksl_hlsl_tests_sources = sksl_blend_tests + sksl_shared_tests
89
90sksl_wgsl_tests_sources =
91    sksl_blend_tests + sksl_compute_tests + sksl_folding_tests +
92    sksl_shared_tests + sksl_wgsl_tests
93
94sksl_spirv_tests_sources =
95    sksl_blend_tests + sksl_compute_tests + sksl_shared_tests + sksl_spirv_tests
96
97sksl_skrp_tests_sources = sksl_folding_tests + sksl_rte_tests + sksl_shared_tests
98
99sksl_stage_tests_sources =
100    sksl_rte_tests + sksl_mesh_tests + sksl_mesh_error_tests
101
102sksl_minify_tests_sources = sksl_folding_tests + sksl_mesh_tests + sksl_rte_tests`
103
104// The footer written to modules/skshaper/skshaper.gni.
105const skshaperFooter = `
106declare_args() {
107  skia_enable_skshaper = true
108}
109declare_args() {
110  skia_enable_skshaper_tests = skia_enable_skshaper
111}`
112
113const portsFooter = `
114skia_fontations_path_bridge_sources = [
115  "$_src/ports/fontations/src/skpath_bridge.h"
116]
117
118skia_fontations_bridge_sources = [
119  "$_src/ports/fontations/src/ffi.rs"
120]
121
122skia_fontations_bridge_root = "$_src/ports/fontations/src/ffi.rs"
123`
124
125// Map of GNI file names to footer text to be appended to the end of the file.
126var footerMap = map[string]string{
127	"gn/codec.gni":                  codecGNIFooter,
128	"gn/core.gni":                   coreGNIFooter,
129	"gn/ports.gni":                  portsFooter,
130	"gn/sksl_tests.gni":             skslTestsFooter,
131	"modules/skshaper/skshaper.gni": skshaperFooter,
132}
133
134// Match variable definition of a list in a *.gni file. For example:
135//
136//	foo = []
137//
138// will match "foo"
139var gniVariableDefReg = regexp.MustCompile(`^(\w+)\s?=\s?\[`)
140
141// NewGNIExporter creates an exporter that will export to GN's (*.gni) files.
142func NewGNIExporter(params GNIExporterParams, filesystem interfaces.FileSystem) *GNIExporter {
143	e := &GNIExporter{
144		workspaceDir:   params.WorkspaceDir,
145		fs:             filesystem,
146		exportGNIDescs: params.ExportDescs,
147	}
148	return e
149}
150
151func makeGniFileContents() gniFileContents {
152	return gniFileContents{
153		bazelFiles: make(map[string]bool),
154	}
155}
156
157// Given a Bazel rule name find that rule from within the
158// query results. Returns nil if the given rule is not present.
159func findQueryResultRule(qr *build.QueryResult, name string) *build.Rule {
160	for _, target := range qr.GetTarget() {
161		r := target.GetRule()
162		if r.GetName() == name {
163			return r
164		}
165	}
166	return nil
167}
168
169// Given a relative path to a file return the relative path to the
170// top directory (in our case the workspace). For example:
171//
172//	getPathToTopDir("path/to/file.h") -> "../.."
173//
174// The paths are to be delimited by forward slashes ('/') - even on
175// Windows.
176func getPathToTopDir(path string) string {
177	if filepath.IsAbs(path) {
178		return ""
179	}
180	d, _ := filepath.Split(path)
181	if d == "" {
182		return "."
183	}
184	d = strings.TrimSuffix(d, "/")
185	items := strings.Split(d, "/")
186	var sb = strings.Builder{}
187	for i := 0; i < len(items); i++ {
188		if i > 0 {
189			sb.WriteString("/")
190		}
191		sb.WriteString("..")
192	}
193	return sb.String()
194}
195
196// Retrieve all rule attributes which are internal file targets.
197func getRuleFiles(r *build.Rule, attrName string) ([]string, error) {
198	items, err := getRuleStringArrayAttribute(r, attrName)
199	if err != nil {
200		return nil, skerr.Wrap(err)
201	}
202
203	var files []string
204	for _, item := range items {
205		if !isExternalRule(item) && isFileTarget(item) {
206			files = append(files, item)
207		}
208	}
209	return files, nil
210}
211
212// Convert a file path into a workspace relative path using variables to
213// specify the base folder. The variables are one of $_src, $_include, or $_modules.
214func makeRelativeFilePathForGNI(path string) (string, error) {
215	if strings.HasPrefix(path, "src/") {
216		return "$_src/" + strings.TrimPrefix(path, "src/"), nil
217	}
218	if strings.HasPrefix(path, "include/") {
219		return "$_include/" + strings.TrimPrefix(path, "include/"), nil
220	}
221	if strings.HasPrefix(path, "modules/") {
222		return "$_modules/" + strings.TrimPrefix(path, "modules/"), nil
223	}
224	if strings.HasPrefix(path, "experimental/") {
225		return "$_experimental/" + strings.TrimPrefix(path, "experimental/"), nil
226	}
227	// These sksl tests are purposely listed as a relative path underneath resources/sksl because
228	// that relative path is re-used by the GN logic to put stuff under //tests/sksl as well.
229	if strings.HasPrefix(path, "resources/sksl/") {
230		return strings.TrimPrefix(path, "resources/sksl/"), nil
231	}
232
233	return "", skerr.Fmt("can't find path for %q\n", path)
234}
235
236// Convert a slice of workspace relative paths into a new slice containing
237// GNI variables ($_src, $_include, etc.). *All* paths in the supplied
238// slice must be a supported top-level directory.
239func addGNIVariablesToWorkspacePaths(paths []string) ([]string, error) {
240	vars := make([]string, 0, len(paths))
241	for _, path := range paths {
242		withVar, err := makeRelativeFilePathForGNI(path)
243		if err != nil {
244			return nil, skerr.Wrap(err)
245		}
246		vars = append(vars, withVar)
247	}
248	return vars, nil
249}
250
251// Is the file path a C++ header?
252func isHeaderFile(path string) bool {
253	ext := strings.ToLower(filepath.Ext(path))
254	return ext == ".h" || ext == ".hpp"
255}
256
257// Does the list of file paths contain only header files?
258func fileListContainsOnlyCppHeaderFiles(files []string) bool {
259	for _, f := range files {
260		if !isHeaderFile(f) {
261			return false
262		}
263	}
264	return len(files) > 0 // Empty list is false, else all are headers.
265}
266
267// Write the *.gni file header.
268func writeGNFileHeader(writer interfaces.Writer, gniFile *gniFileContents, pathToWorkspace string) {
269	_, _ = fmt.Fprintln(writer, "# DO NOT EDIT: This is a generated file.")
270	_, _ = fmt.Fprintln(writer, "# See //bazel/exporter_tool/README.md for more information.")
271
272	_, _ = fmt.Fprintln(writer, "#")
273	if len(gniFile.bazelFiles) > 1 {
274		keys := make([]string, 0, len(gniFile.bazelFiles))
275		_, _ = fmt.Fprintln(writer, "# The sources of truth are:")
276		for bazelPath := range gniFile.bazelFiles {
277			keys = append(keys, bazelPath)
278		}
279		sort.Strings(keys)
280		for _, wsPath := range keys {
281			_, _ = fmt.Fprintf(writer, "#   //%s\n", wsPath)
282		}
283	} else {
284		for bazelPath := range gniFile.bazelFiles {
285			_, _ = fmt.Fprintf(writer, "# The source of truth is //%s\n", bazelPath)
286		}
287	}
288
289	_, _ = writer.WriteString("\n")
290	_, _ = fmt.Fprintln(writer, "# To update this file, run make -C bazel generate_gni")
291
292	_, _ = writer.WriteString("\n")
293	if gniFile.hasSrcs {
294		_, _ = fmt.Fprintf(writer, "_src = get_path_info(\"%s/src\", \"abspath\")\n", pathToWorkspace)
295	}
296	if gniFile.hasExperimental {
297		_, _ = fmt.Fprintf(writer, "_experimental = get_path_info(\"%s/experimental\", \"abspath\")\n", pathToWorkspace)
298	}
299	if gniFile.hasIncludes {
300		_, _ = fmt.Fprintf(writer, "_include = get_path_info(\"%s/include\", \"abspath\")\n", pathToWorkspace)
301	}
302	if gniFile.hasModules {
303		_, _ = fmt.Fprintf(writer, "_modules = get_path_info(\"%s/modules\", \"abspath\")\n", pathToWorkspace)
304	}
305}
306
307// removeDuplicates returns the list of files after it has been sorted and
308// all duplicate values have been removed.
309func removeDuplicates(files []string) []string {
310	if len(files) <= 1 {
311		return files
312	}
313	sort.Strings(files)
314	rv := make([]string, 0, len(files))
315	rv = append(rv, files[0])
316	for _, f := range files {
317		if rv[len(rv)-1] != f {
318			rv = append(rv, f)
319		}
320	}
321	return rv
322}
323
324// Retrieve all sources ("srcs" attribute) and headers ("hdrs" attribute)
325// and return as a single slice of target names. Slice entries will be
326// something like:
327//
328//	"//src/core/file.cpp".
329func getSrcsAndHdrs(r *build.Rule) ([]string, error) {
330	srcs, err := getRuleFiles(r, "srcs")
331	if err != nil {
332		return nil, skerr.Wrap(err)
333	}
334
335	hdrs, err := getRuleFiles(r, "hdrs")
336	if err != nil {
337		return nil, skerr.Wrap(err)
338	}
339	return append(srcs, hdrs...), nil
340}
341
342// Convert a slice of file path targets to workspace relative file paths.
343// i.e. convert each element like:
344//
345//	"//src/core/file.cpp"
346//
347// into:
348//
349//	"src/core/file.cpp"
350func convertTargetsToFilePaths(targets []string) ([]string, error) {
351	paths := make([]string, 0, len(targets))
352	for _, target := range targets {
353		path, err := getFilePathFromFileTarget(target)
354		if err != nil {
355			return nil, skerr.Wrap(err)
356		}
357		paths = append(paths, path)
358	}
359	return paths, nil
360}
361
362// Return the top-level component (directory or file) of a relative file path.
363// The paths are assumed to be delimited by forward slash (/) characters (even on Windows).
364// An empty string is returned if no top level folder can be found.
365//
366// Example:
367//
368//	"foo/bar/baz.txt" returns "foo"
369func extractTopLevelFolder(path string) string {
370	parts := strings.Split(path, "/")
371	if len(parts) > 0 {
372		return parts[0]
373	}
374	return ""
375}
376
377// Extract the name of a variable assignment from a line of text from a GNI file.
378// So, a line like:
379//
380//	"foo = [...]"
381//
382// will return:
383//
384//	"foo"
385func getGNILineVariable(line string) string {
386	if matches := gniVariableDefReg.FindStringSubmatch(line); matches != nil {
387		return matches[1]
388	}
389	return ""
390}
391
392// Given a workspace relative path return an absolute path.
393func (e *GNIExporter) workspaceToAbsPath(wsPath string) string {
394	if filepath.IsAbs(wsPath) {
395		panic("filepath already absolute")
396	}
397	return filepath.Join(e.workspaceDir, wsPath)
398}
399
400// Given an absolute path return a workspace relative path.
401func (e *GNIExporter) absToWorkspacePath(absPath string) (string, error) {
402	if !filepath.IsAbs(absPath) {
403		return "", skerr.Fmt(`"%s" is not an absolute path`, absPath)
404	}
405	if absPath == e.workspaceDir {
406		return "", nil
407	}
408	wsDir := e.workspaceDir + "/"
409	if !strings.HasPrefix(absPath, wsDir) {
410		return "", skerr.Fmt(`"%s" is not in the workspace "%s"`, absPath, wsDir)
411	}
412	return strings.TrimPrefix(absPath, wsDir), nil
413}
414
415// Merge the another file contents object into this one.
416func (c *gniFileContents) merge(other gniFileContents) {
417	if other.hasExperimental {
418		c.hasExperimental = true
419	}
420	if other.hasIncludes {
421		c.hasIncludes = true
422	}
423	if other.hasModules {
424		c.hasModules = true
425	}
426	if other.hasSrcs {
427		c.hasSrcs = true
428	}
429	for path := range other.bazelFiles {
430		c.bazelFiles[path] = true
431	}
432	c.data = append(c.data, other.data...)
433}
434
435// Convert all rules that go into a GNI file list.
436func (e *GNIExporter) convertGNIFileList(desc GNIFileListExportDesc, qr *build.QueryResult) (gniFileContents, error) {
437	var rules []string
438	fileContents := makeGniFileContents()
439	var targets []string
440	for _, ruleName := range desc.Rules {
441		r := findQueryResultRule(qr, ruleName)
442		if r == nil {
443			return gniFileContents{}, skerr.Fmt("Cannot find rule %s", ruleName)
444		}
445		absBazelPath, _, _, err := parseLocation(*r.Location)
446		if err != nil {
447			return gniFileContents{}, skerr.Wrap(err)
448		}
449		wsBazelpath, err := e.absToWorkspacePath(absBazelPath)
450		if err != nil {
451			return gniFileContents{}, skerr.Wrap(err)
452		}
453		fileContents.bazelFiles[wsBazelpath] = true
454		t, err := getSrcsAndHdrs(r)
455		if err != nil {
456			return gniFileContents{}, skerr.Wrap(err)
457		}
458		if len(t) == 0 {
459			return gniFileContents{}, skerr.Fmt("No files to export in rule %s", ruleName)
460		}
461		targets = append(targets, t...)
462		rules = append(rules, ruleName)
463	}
464
465	files, err := convertTargetsToFilePaths(targets)
466	if err != nil {
467		return gniFileContents{}, skerr.Wrap(err)
468	}
469
470	files, err = addGNIVariablesToWorkspacePaths(files)
471	if err != nil {
472		return gniFileContents{}, skerr.Wrap(err)
473	}
474
475	files = removeDuplicates(files)
476
477	for i := range files {
478		if strings.HasPrefix(files[i], "$_src/") {
479			fileContents.hasSrcs = true
480		} else if strings.HasPrefix(files[i], "$_experimental/") {
481			fileContents.hasExperimental = true
482		} else if strings.HasPrefix(files[i], "$_include/") {
483			fileContents.hasIncludes = true
484		} else if strings.HasPrefix(files[i], "$_modules/") {
485			fileContents.hasModules = true
486		}
487	}
488
489	var contents bytes.Buffer
490
491	if len(rules) > 1 {
492		_, _ = fmt.Fprintln(&contents, "# List generated by Bazel rules:")
493		for _, bazelFile := range rules {
494			_, _ = fmt.Fprintf(&contents, "#  %s\n", bazelFile)
495		}
496	} else if len(rules) > 0 {
497		_, _ = fmt.Fprintf(&contents, "# Generated by Bazel rule %s\n", rules[0])
498	}
499	_, _ = fmt.Fprintf(&contents, "%s = [\n", desc.Var)
500
501	for _, target := range files {
502		_, _ = fmt.Fprintf(&contents, "  %q,\n", target)
503	}
504	_, _ = fmt.Fprintln(&contents, "]")
505	_, _ = fmt.Fprintln(&contents)
506	fileContents.data = contents.Bytes()
507
508	return fileContents, nil
509}
510
511// Export all Bazel rules to a single *.gni file.
512func (e *GNIExporter) exportGNIFile(gniExportDesc GNIExportDesc, qr *build.QueryResult) error {
513	// Keep the contents of each file list in memory before writing to disk.
514	// This is done so that we know what variables to define for each of the
515	// file lists. i.e. $_src, $_include, etc.
516	gniFileContents := makeGniFileContents()
517	for _, varDesc := range gniExportDesc.Vars {
518		fileListContents, err := e.convertGNIFileList(varDesc, qr)
519		if err != nil {
520			return skerr.Wrap(err)
521		}
522		gniFileContents.merge(fileListContents)
523	}
524
525	writer, err := e.fs.OpenFile(e.workspaceToAbsPath(gniExportDesc.GNI))
526	if err != nil {
527		return skerr.Wrap(err)
528	}
529
530	pathToWorkspace := getPathToTopDir(gniExportDesc.GNI)
531	writeGNFileHeader(writer, &gniFileContents, pathToWorkspace)
532	_, _ = writer.WriteString("\n")
533
534	_, err = writer.Write(gniFileContents.data)
535	if err != nil {
536		return skerr.Wrap(err)
537	}
538
539	for gniPath, footer := range footerMap {
540		if gniExportDesc.GNI == gniPath {
541			_, _ = fmt.Fprintln(writer, footer)
542			break
543		}
544	}
545
546	return nil
547}
548
549// Export the contents of a Bazel query response to one or more GNI
550// files.
551//
552// The Bazel data to export, and the destination GNI files are defined
553// by the configuration data supplied to NewGNIExporter().
554func (e *GNIExporter) Export(qcmd interfaces.QueryCommand) error {
555	in, err := qcmd.Read()
556	if err != nil {
557		return skerr.Wrapf(err, "error reading bazel cquery data")
558	}
559	qr := &build.QueryResult{}
560	if err := proto.Unmarshal(in, qr); err != nil {
561		return skerr.Wrapf(err, "failed to unmarshal cquery result")
562	}
563	for _, desc := range e.exportGNIDescs {
564		err = e.exportGNIFile(desc, qr)
565		if err != nil {
566			return skerr.Wrap(err)
567		}
568	}
569	return nil
570}
571
572// Make sure GNIExporter fulfills the Exporter interface.
573var _ interfaces.Exporter = (*GNIExporter)(nil)
574