1// Copyright 2022 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package main 6 7import ( 8 "flag" 9 "fmt" 10 "io" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "regexp" 15 "sort" 16 "strings" 17) 18 19type depConfig struct { 20 bazelNameOverride string // Bazel style uses underscores not dashes, so we fix those if needed. 21 needsBazelFile bool 22 patchCmds []string 23 patchCmdsWin []string 24} 25 26// These are all C++ deps or Rust deps (with a compatible C++ FFI) used by the Bazel build. 27// They are a subset of those listed in DEPS. 28// The key is the name of the repo as specified in DEPS. 29var deps = map[string]depConfig{ 30 "abseil-cpp": {bazelNameOverride: "abseil_cpp"}, 31 "brotli": {}, 32 "highway": {}, 33 "spirv-tools": {bazelNameOverride: "spirv_tools"}, 34 // This name is important because spirv_tools expects @spirv_headers to exist by that name. 35 "spirv-headers": {bazelNameOverride: "spirv_headers"}, 36 37 "dawn": {needsBazelFile: true}, 38 "delaunator-cpp": {bazelNameOverride: "delaunator", needsBazelFile: true}, 39 "dng_sdk": {needsBazelFile: true}, 40 "expat": {needsBazelFile: true}, 41 "freetype": {needsBazelFile: true}, 42 "harfbuzz": {needsBazelFile: true}, 43 "icu": { 44 needsBazelFile: true, 45 patchCmds: []string{ 46 `"rm source/i18n/BUILD.bazel"`, 47 `"rm source/common/BUILD.bazel"`, 48 `"rm source/stubdata/BUILD.bazel"`, 49 }, 50 patchCmdsWin: []string{ 51 `"del source/i18n/BUILD.bazel"`, 52 `"del source/common/BUILD.bazel"`, 53 `"del source/stubdata/BUILD.bazel"`, 54 }, 55 }, 56 "icu4x": {needsBazelFile: true}, 57 "imgui": {needsBazelFile: true}, 58 "libavif": {needsBazelFile: true}, 59 "libgav1": {needsBazelFile: true}, 60 "libjpeg-turbo": {bazelNameOverride: "libjpeg_turbo", needsBazelFile: true}, 61 "libjxl": {needsBazelFile: true}, 62 "libpng": {needsBazelFile: true}, 63 "libwebp": {needsBazelFile: true}, 64 "libyuv": {needsBazelFile: true}, 65 "spirv-cross": {bazelNameOverride: "spirv_cross", needsBazelFile: true}, 66 "perfetto": {needsBazelFile: true}, 67 "piex": {needsBazelFile: true}, 68 "vello": {needsBazelFile: true}, 69 "vulkan-headers": {bazelNameOverride: "vulkan_headers", needsBazelFile: true}, 70 "vulkan-tools": {bazelNameOverride: "vulkan_tools", needsBazelFile: true}, 71 "vulkan-utility-libraries": {bazelNameOverride: "vulkan_utility_libraries", needsBazelFile: true}, 72 "vulkanmemoryallocator": {needsBazelFile: true}, 73 "wuffs": {needsBazelFile: true}, 74 // Some other dependency downloads zlib but with their own rules 75 "zlib": {bazelNameOverride: "zlib_skia", needsBazelFile: true}, 76} 77 78func main() { 79 var ( 80 depsFile = flag.String("deps_file", "DEPS", "The location of the DEPS file. Usually at the root of the repository") 81 genBzlFile = flag.String("gen_bzl_file", "bazel/deps.bzl", "The location of the .bzl file that has the generated Bazel repository rules.") 82 workspaceFile = flag.String("workspace_file", "WORKSPACE.bazel", "The location of the WORKSPACE file that should be updated with dep names.") 83 // https://bazel.build/docs/user-manual#running-executables 84 repoDir = flag.String("repo_dir", os.Getenv("BUILD_WORKSPACE_DIRECTORY"), "The root directory of the repo. Default set by BUILD_WORKSPACE_DIRECTORY env variable.") 85 buildifierPath = flag.String("buildifier", "", "Where to find buildifier. Defaults to Bazel's location") 86 ) 87 flag.Parse() 88 89 if *repoDir == "" { 90 fmt.Println(`Must set --repo_dir 91This is done automatically via: 92 bazel run //bazel/deps_parser`) 93 os.Exit(1) 94 } 95 96 buildifier := *buildifierPath 97 if buildifier == "" { 98 // We don't know if this will be buildifier_linux_x64, buildifier_macos_arm64, etc 99 bp, err := filepath.Glob("../buildifier*/file/buildifier") 100 if err != nil || len(bp) != 1 { 101 fmt.Printf("Could not find exactly one buildifier executable %s %v\n", err, bp) 102 os.Exit(1) 103 } 104 buildifier = bp[0] 105 } 106 buildifier, err := filepath.Abs(buildifier) 107 if err != nil { 108 fmt.Printf("Abs path error %s\n", err) 109 os.Exit(1) 110 } 111 112 fmt.Println(os.Environ()) 113 114 if *depsFile == "" || *genBzlFile == "" { 115 fmt.Println("Must set --deps_file and --gen_bzl_file") 116 flag.PrintDefaults() 117 } 118 119 if err := os.Chdir(*repoDir); err != nil { 120 fmt.Printf("Could not cd to %s\n", *repoDir) 121 os.Exit(1) 122 } 123 124 b, err := os.ReadFile(*depsFile) 125 if err != nil { 126 fmt.Printf("Could not open %s: %s\n", *depsFile, err) 127 os.Exit(1) 128 } 129 contents := strings.Split(string(b), "\n") 130 131 outputFile, count, err := parseDEPSFile(contents, *workspaceFile) 132 if err != nil { 133 fmt.Printf("Parsing error %s\n", err) 134 os.Exit(1) 135 } 136 if err := exec.Command(buildifier, outputFile).Run(); err != nil { 137 fmt.Printf("Buildifier error %s\n", err) 138 os.Exit(1) 139 } 140 if err := moveWithCopyBackup(outputFile, *genBzlFile); err != nil { 141 fmt.Printf("Could not write comments in workspace file: %s\n", err) 142 os.Exit(1) 143 } 144 fmt.Printf("Wrote %d deps\n", count) 145} 146 147func parseDEPSFile(contents []string, workspaceFile string) (string, int, error) { 148 depsLine := regexp.MustCompile(`externals/(\S+)".+"(https.+)@([a-f0-9]+)"`) 149 outputFile, err := os.CreateTemp("", "genbzl") 150 if err != nil { 151 return "", 0, fmt.Errorf("Could not create output file: %s\n", err) 152 } 153 defer outputFile.Close() 154 155 if _, err := outputFile.WriteString(header); err != nil { 156 return "", 0, fmt.Errorf("Could not write header to output file %s: %s\n", outputFile.Name(), err) 157 } 158 159 var nativeRepos []string 160 var providedRepos []string 161 162 count := 0 163 for _, line := range contents { 164 if match := depsLine.FindStringSubmatch(line); len(match) > 0 { 165 id := match[1] 166 repo := match[2] 167 rev := match[3] 168 169 cfg, ok := deps[id] 170 if !ok { 171 continue 172 } 173 if cfg.bazelNameOverride != "" { 174 id = cfg.bazelNameOverride 175 } 176 if cfg.needsBazelFile { 177 if err := writeNewGitRepositoryRule(outputFile, id, repo, rev, cfg.patchCmds, cfg.patchCmdsWin); err != nil { 178 return "", 0, fmt.Errorf("Could not write to output file %s: %s\n", outputFile.Name(), err) 179 } 180 workspaceLine := fmt.Sprintf("# @%s - //bazel/external/%s:BUILD.bazel", id, id) 181 providedRepos = append(providedRepos, workspaceLine) 182 } else { 183 if err := writeGitRepositoryRule(outputFile, id, repo, rev); err != nil { 184 return "", 0, fmt.Errorf("Could not write to output file %s: %s\n", outputFile.Name(), err) 185 } 186 workspaceLine := fmt.Sprintf("# @%s - %s", id, repo) 187 nativeRepos = append(nativeRepos, workspaceLine) 188 } 189 count++ 190 } 191 } 192 if count != len(deps) { 193 return "", 0, fmt.Errorf("Not enough deps written. Maybe the deps dictionary needs a bazelNameOverride or an old dep needs to be removed?") 194 } 195 196 if _, err := outputFile.WriteString(footer); err != nil { 197 return "", 0, fmt.Errorf("Could not write footer to output file %s: %s\n", outputFile.Name(), err) 198 } 199 200 if newWorkspaceFile, err := writeCommentsToWorkspace(workspaceFile, nativeRepos, providedRepos); err != nil { 201 fmt.Printf("Could not parse workspace file %s: %s\n", workspaceFile, err) 202 os.Exit(1) 203 } else { 204 if err := moveWithCopyBackup(newWorkspaceFile, workspaceFile); err != nil { 205 fmt.Printf("Could not write comments in workspace file: %s\n", err) 206 os.Exit(1) 207 } 208 } 209 return outputFile.Name(), count, nil 210} 211 212func writeCommentsToWorkspace(workspaceFile string, nativeRepos, providedRepos []string) (string, error) { 213 b, err := os.ReadFile(workspaceFile) 214 if err != nil { 215 return "", fmt.Errorf("Could not open %s: %s\n", workspaceFile, err) 216 } 217 newWorkspace, err := os.CreateTemp("", "workspace") 218 if err != nil { 219 return "", fmt.Errorf("Could not make tempfile: %s\n", err) 220 } 221 defer newWorkspace.Close() 222 223 workspaceContents := strings.Split(string(b), "\n") 224 225 sort.Strings(nativeRepos) 226 sort.Strings(providedRepos) 227 for _, line := range workspaceContents { 228 if _, err := newWorkspace.WriteString(line + "\n"); err != nil { 229 return "", err 230 } 231 if line == startListString { 232 break 233 } 234 } 235 for _, repoLine := range nativeRepos { 236 if _, err := newWorkspace.WriteString(repoLine + "\n"); err != nil { 237 return "", err 238 } 239 } 240 if _, err := newWorkspace.WriteString("#\n"); err != nil { 241 return "", err 242 } 243 for _, repoLine := range providedRepos { 244 if _, err := newWorkspace.WriteString(repoLine + "\n"); err != nil { 245 return "", err 246 } 247 } 248 if _, err := newWorkspace.WriteString(endListString + "\n"); err != nil { 249 return "", err 250 } 251 252 pastEnd := false 253 // Skip the last line, which is blank. We don't want to end with two empty newlines. 254 for _, line := range workspaceContents[:len(workspaceContents)-1] { 255 if line == endListString { 256 pastEnd = true 257 continue 258 } 259 if !pastEnd { 260 continue 261 } 262 if _, err := newWorkspace.WriteString(line + "\n"); err != nil { 263 return "", err 264 } 265 } 266 267 return newWorkspace.Name(), nil 268} 269 270const ( 271 startListString = `#### START GENERATED LIST OF THIRD_PARTY DEPS` 272 endListString = `#### END GENERATED LIST OF THIRD_PARTY DEPS` 273) 274 275const header = `""" 276This file is auto-generated from //bazel/deps_parser 277DO NOT MODIFY BY HAND. 278Instead, do: 279 bazel run //bazel/deps_parser 280""" 281 282load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository", "new_git_repository") 283load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 284load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") 285load("//bazel:download_config_files.bzl", "download_config_files") 286load("//bazel:gcs_mirror.bzl", "gcs_mirror_url") 287 288def c_plus_plus_deps(ws = "@skia"): 289 """A list of native Bazel git rules to download third party git repositories 290 291 These are in the order they appear in //DEPS. 292 https://bazel.build/rules/lib/repo/git 293 294 Args: 295 ws: The name of the Skia Bazel workspace. The default, "@", may be when used from within the 296 Skia workspace. 297 """` 298 299// If necessary, we can make a new map for bazel deps 300const footer = ` 301def bazel_deps(): 302 maybe( 303 http_archive, 304 name = "bazel_skylib", 305 sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d", 306 urls = gcs_mirror_url( 307 sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d", 308 url = "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz", 309 ), 310 ) 311 312 maybe( 313 http_archive, 314 name = "bazel_toolchains", 315 sha256 = "e52789d4e89c3e2dc0e3446a9684626a626b6bec3fde787d70bae37c6ebcc47f", 316 strip_prefix = "bazel-toolchains-5.1.1", 317 urls = gcs_mirror_url( 318 sha256 = "e52789d4e89c3e2dc0e3446a9684626a626b6bec3fde787d70bae37c6ebcc47f", 319 url = "https://github.com/bazelbuild/bazel-toolchains/archive/refs/tags/v5.1.1.tar.gz", 320 ), 321 ) 322 323def header_based_configs(): 324 skia_revision = "d211141c45c9171437fa8e6e07989edb5bffa17a" 325 maybe( 326 download_config_files, 327 name = "expat_config", 328 skia_revision = skia_revision, 329 files = { 330 "BUILD.bazel": "third_party/expat/include/BUILD.bazel", 331 "expat_config/expat_config.h": "third_party/expat/include/expat_config/expat_config.h", 332 }, 333 ) 334 maybe( 335 download_config_files, 336 name = "freetype_config", 337 skia_revision = skia_revision, 338 files = { 339 "BUILD.bazel": "third_party/freetype2/include/BUILD.bazel", 340 "freetype-android/freetype/config/ftmodule.h": "third_party/freetype2/include/freetype-android/freetype/config/ftmodule.h", 341 "freetype-android/freetype/config/ftoption.h": "third_party/freetype2/include/freetype-android/freetype/config/ftoption.h", 342 "freetype-no-type1/freetype/config/ftmodule.h": "third_party/freetype2/include/freetype-no-type1/freetype/config/ftmodule.h", 343 "freetype-no-type1/freetype/config/ftoption.h": "third_party/freetype2/include/freetype-no-type1/freetype/config/ftoption.h", 344 }, 345 ) 346 maybe( 347 download_config_files, 348 name = "harfbuzz_config", 349 skia_revision = skia_revision, 350 files = { 351 "BUILD.bazel": "third_party/harfbuzz/BUILD.bazel", 352 "config-override.h": "third_party/harfbuzz/config-override.h", 353 }, 354 ) 355 maybe( 356 download_config_files, 357 name = "icu_utils", 358 skia_revision = skia_revision, 359 files = { 360 "BUILD.bazel": "third_party/icu/BUILD.bazel", 361 "SkLoadICU.cpp": "third_party/icu/SkLoadICU.cpp", 362 "SkLoadICU.h": "third_party/icu/SkLoadICU.h", 363 "make_data_cpp.py": "third_party/icu/make_data_cpp.py", 364 }, 365 ) 366` 367 368func writeNewGitRepositoryRule(w io.StringWriter, bazelName, repo, rev string, patchCmds, patchCmdsWin []string) error { 369 if len(patchCmds) == 0 { 370 // TODO(kjlubick) In a newer version of Bazel, new_git_repository can be replaced with just 371 // git_repository 372 _, err := w.WriteString(fmt.Sprintf(` 373 new_git_repository( 374 name = "%s", 375 build_file = ws + "//bazel/external/%s:BUILD.bazel", 376 commit = "%s", 377 remote = "%s", 378 ) 379`, bazelName, bazelName, rev, repo)) 380 return err 381 } 382 patches := "[" + strings.Join(patchCmds, ",\n") + "]" 383 patches_win := "[" + strings.Join(patchCmdsWin, ",\n") + "]" 384 _, err := w.WriteString(fmt.Sprintf(` 385 new_git_repository( 386 name = "%s", 387 build_file = ws + "//bazel/external/%s:BUILD.bazel", 388 commit = "%s", 389 remote = "%s", 390 patch_cmds = %s, 391 patch_cmds_win = %s, 392 ) 393`, bazelName, bazelName, rev, repo, patches, patches_win)) 394 return err 395} 396 397func writeGitRepositoryRule(w io.StringWriter, bazelName, repo, rev string) error { 398 _, err := w.WriteString(fmt.Sprintf(` 399 git_repository( 400 name = "%s", 401 commit = "%s", 402 remote = "%s", 403 ) 404`, bazelName, rev, repo)) 405 return err 406} 407 408func moveWithCopyBackup(src, dst string) error { 409 // Atomically rename temp file to workspace. This should minimize the chance of corruption 410 // or writing a partial file if there is an error or the program is interrupted. 411 if err := os.Rename(src, dst); err != nil { 412 // Errors can happen if the temporary file is on a different partition than the Skia 413 // codebase. In that case, do a manual read/write to copy the data. See 414 // https://github.com/jenkins-x/jx/issues/449 for a similar issue 415 if strings.Contains(err.Error(), "invalid cross-device link") { 416 bytes, err := os.ReadFile(src) 417 if err != nil { 418 return fmt.Errorf("Could not do backup read from %s: %s\n", src, err) 419 } 420 if err := os.WriteFile(dst, bytes, 0644); err != nil { 421 return fmt.Errorf("Could not do backup write of %d bytes to %s: %s\n", len(bytes), dst, err) 422 } 423 // Backup "move" successful 424 return nil 425 } 426 return fmt.Errorf("Could not write %s -> %s: %s\n", src, dst, err) 427 } 428 return nil 429} 430