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