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