Philipp Schrader | 272e07b | 2024-03-29 17:26:02 -0700 | [diff] [blame^] | 1 | import os |
| 2 | import subprocess |
| 3 | import sys |
| 4 | import tempfile |
| 5 | from pathlib import Path |
| 6 | |
| 7 | import yaml |
| 8 | from python.runfiles import runfiles |
| 9 | |
| 10 | RUNFILES = runfiles.Create() |
| 11 | |
| 12 | FAKE_NPM_BIN = RUNFILES.Rlocation( |
| 13 | "org_frc971/tools/foxglove/creation_wrapper_npm") |
| 14 | BUILDOZER_BIN = RUNFILES.Rlocation( |
| 15 | "com_github_bazelbuild_buildtools/buildozer/buildozer_/buildozer") |
| 16 | |
| 17 | WORKSPACE_DIR = Path(os.environ["BUILD_WORKSPACE_DIRECTORY"]) |
| 18 | WORKING_DIR = Path(os.environ["BUILD_WORKING_DIRECTORY"]) |
| 19 | |
| 20 | |
| 21 | def create_npm_link(temp_dir: Path, env: dict[str, str]): |
| 22 | """Set up the creation_wrapper_npm.py script as the "npm" binary.""" |
| 23 | bin_dir = temp_dir / "bin" |
| 24 | bin_dir.mkdir() |
| 25 | npm = bin_dir / "npm" |
| 26 | npm.symlink_to(FAKE_NPM_BIN) |
| 27 | env["PATH"] = f"{temp_dir / 'bin'}:{env['PATH']}" |
| 28 | |
| 29 | |
| 30 | def run_create_foxglove_extension(argv: list[str], name: str): |
| 31 | """Runs the create-foxglove-extension binary. |
| 32 | |
| 33 | Args: |
| 34 | argv: The list of command line arguments passed to this wrapper. |
| 35 | name: The (directory) name of the new extension to be created. |
| 36 | """ |
| 37 | with tempfile.TemporaryDirectory() as temp_dir: |
| 38 | temp_dir = Path(temp_dir) |
| 39 | env = os.environ.copy() |
| 40 | create_npm_link(temp_dir, env) |
| 41 | |
| 42 | env["BAZEL_BINDIR"] = WORKING_DIR |
| 43 | env.pop("RUNFILES_DIR", None) |
| 44 | env.pop("RUNFILES_MANIFEST_FILE", None) |
| 45 | |
| 46 | subprocess.run(argv[1:], check=True, env=env, cwd=WORKING_DIR) |
| 47 | # For some reason, the `foxglove-extension` binary doesn't set up the |
| 48 | # ts-loader dependency. Do it manually here. |
| 49 | subprocess.run(["npm", "install", "ts-loader@^9"], |
| 50 | check=True, |
| 51 | env=env, |
| 52 | cwd=WORKING_DIR / name) |
| 53 | |
| 54 | |
| 55 | def add_new_js_project(name: str): |
| 56 | """Tell Bazel about the new project.""" |
| 57 | # The name of the Bazel package for the new extension. |
| 58 | package_name = WORKING_DIR.relative_to(WORKSPACE_DIR) / name |
| 59 | |
| 60 | # Add the new "node_modules" directory to the ignore list. |
| 61 | bazelignore_file = WORKSPACE_DIR / ".bazelignore" |
| 62 | bazelignore = bazelignore_file.read_text() |
| 63 | bazelignore_entry = str(package_name / "node_modules") |
| 64 | if bazelignore_entry not in bazelignore.splitlines(): |
| 65 | bazelignore = bazelignore.rstrip("\n") + "\n" |
| 66 | bazelignore_file.write_text(bazelignore + bazelignore_entry + "\n") |
| 67 | |
| 68 | # Add the new project to the workspace list. This ensures the lock file |
| 69 | # gets updated properly. |
| 70 | pnpm_workspace_file = WORKSPACE_DIR / "pnpm-workspace.yaml" |
| 71 | pnpm_workspace = yaml.load(pnpm_workspace_file.read_text(), |
| 72 | Loader=yaml.CLoader) |
| 73 | if str(package_name) not in pnpm_workspace["packages"]: |
| 74 | pnpm_workspace["packages"].append(str(package_name)) |
| 75 | pnpm_workspace_file.write_text(yaml.dump(pnpm_workspace)) |
| 76 | |
| 77 | # Add the new project to the workspace. This ensures that all of its |
| 78 | # dependencies get downloaded by Bazel. |
| 79 | subprocess.check_call([ |
| 80 | BUILDOZER_BIN, |
| 81 | f"add data @//{package_name}:package.json", |
| 82 | "WORKSPACE:npm", |
| 83 | ], |
| 84 | cwd=WORKSPACE_DIR) |
| 85 | |
| 86 | # Regenerate the lock file with the new project's dependencies included. |
| 87 | subprocess.check_call([ |
| 88 | "bazel", |
| 89 | "run", |
| 90 | "--", |
| 91 | "@pnpm//:pnpm", |
| 92 | "--dir", |
| 93 | WORKSPACE_DIR, |
| 94 | "install", |
| 95 | "--lockfile-only", |
| 96 | ], |
| 97 | cwd=WORKSPACE_DIR) |
| 98 | |
| 99 | |
| 100 | def main(argv): |
| 101 | """Runs the main logic.""" |
| 102 | |
| 103 | # Assume that the only argument the user passed in is the name of the |
| 104 | # extension. We can probably do better here, but oh well. |
| 105 | create_foxglove_extension_args = argv[2:] |
| 106 | name = create_foxglove_extension_args[0] |
| 107 | |
| 108 | run_create_foxglove_extension(argv, name) |
| 109 | add_new_js_project(name) |
| 110 | |
| 111 | # Generate a BUILD file. |
| 112 | build_file_template = WORKSPACE_DIR / "tools/foxglove/BUILD.bazel.tmpl" |
| 113 | build_file = WORKING_DIR / name / "BUILD.bazel" |
| 114 | build_file.write_text(build_file_template.read_text()) |
| 115 | |
| 116 | # Fix up the tsconfig.json. For some reason the inheritance for the `lib` |
| 117 | # field doesn't work out of the box. We're using string manipulation since |
| 118 | # we don't have a readily-available "JSON with comments" parser. |
| 119 | tsconfig_file = WORKING_DIR / name / "tsconfig.json" |
| 120 | tsconfig = tsconfig_file.read_text() |
| 121 | tsconfig = tsconfig.replace('"lib": ["dom"]', '"lib": ["dom", "es2022"]') |
| 122 | tsconfig_file.write_text(tsconfig) |
| 123 | |
| 124 | |
| 125 | if __name__ == "__main__": |
| 126 | sys.exit(main(sys.argv)) |