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