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