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