|
| 1 | +--- |
| 2 | +name: bazel |
| 3 | +description: Conventions for editing Bazel files in the github/codeql repository — the shared `misc/bazel` helpers, `codeql_platform_select` and the `{CODEQL_PLATFORM}` packaging placeholder, adding `MODULE.bazel` dependencies, and the `semmle_code` stub that keeps the standalone build working. Use when editing any `BUILD.bazel`, `*.bzl`, `MODULE.bazel` or `.bazelrc` here, and before validating such an edit. |
| 4 | +--- |
| 5 | + |
| 6 | +# Bazel in the codeql repository |
| 7 | + |
| 8 | +A bzlmod module named `ql`, repo name `@codeql` ([`MODULE.bazel`](../../../MODULE.bazel)). It builds standalone, and is |
| 9 | +also consumed by an internal module that depends on it; standalone builds replace that module with a stub. |
| 10 | + |
| 11 | +## Traps |
| 12 | + |
| 13 | +Things that will waste your time or produce a wrong edit here. Read this section even if you skip the rest. |
| 14 | + |
| 15 | +* **`//...` does not work.** `bazel build //...`, and even `bazel query //...`, fail at the repo root — the patched |
| 16 | + modules under `misc/bazel/registry/modules/*/*/overlay` are real packages referencing repos that are not visible from |
| 17 | + the main repo, and [`.bazelrc`](../../../.bazelrc) notes that transitions break `...` builds separately. **Validate |
| 18 | + the specific target or package you changed**, not a recursive pattern. The error names an unrelated directory and is |
| 19 | + very easy to misdiagnose. |
| 20 | +* **There is no `MODULE.bazel.lock`, deliberately.** [`.bazelrc`](../../../.bazelrc) sets `--lockfile_mode=off` because |
| 21 | + the workspace-relative module override makes a lockfile unstable. Do not add one, and do not "fix" its absence. |
| 22 | +* **`linux_arm64` vs `linux-arm64`.** The keyword argument and config setting use an underscore; the platform *string* |
| 23 | + substituted into paths and zip names uses a hyphen. They are not interchangeable. |
| 24 | +* **Do not stub your way out of a missing internal dependency.** See [Building standalone](#building-standalone) for the |
| 25 | + one narrow case where extending the stub is correct. |
| 26 | + |
| 27 | +## Rules of thumb |
| 28 | + |
| 29 | +* **Copy a neighbouring target rather than inventing a shape.** Note that packaging is not uniform — some packages use |
| 30 | + the `codeql_*` wrappers, others still use `pkg_files` directly — so copy the closest *working* neighbour and prefer |
| 31 | + the wrapper for new code. |
| 32 | +* **Pin anything fetched over the network** with `sha256` or `integrity`. Bazel only *warns* on an unpinned download, so |
| 33 | + nothing fails loudly, but the build stops being reproducible and a retagged upstream release silently changes what you |
| 34 | + build. `lfs_archive` is the exception — content is pinned by git object. |
| 35 | +* **Format with `bazel run //misc/bazel/buildifier`.** It rewrites in place, so don't hand-tune formatting. Also wired as |
| 36 | + a `pre-commit` hook ([`.pre-commit-config.yaml`](../../../.pre-commit-config.yaml)). |
| 37 | + |
| 38 | +## Where new code goes |
| 39 | + |
| 40 | +Bazel's own macro / rule / repository-rule distinction applies as usual. What is repo-specific: |
| 41 | + |
| 42 | +| Adding… | Goes in | |
| 43 | +| --- | --- | |
| 44 | +| a new packaging shape | extend [`misc/bazel/pkg.bzl`](../../../misc/bazel/pkg.bzl) — don't fork `pkg_files` | |
| 45 | +| a new OS or arch split | [`misc/bazel/os.bzl`](../../../misc/bazel/os.bzl) — don't hand-roll a `select()` over `@platforms//` | |
| 46 | +| a fetch of something external | a repository rule ([`lfs.bzl`](../../../misc/bazel/lfs.bzl), [`ripunzip.bzl`](../../../misc/ripunzip/ripunzip.bzl)) — not a `genrule` | |
| 47 | +| a wrapper used by one language | next to that language ([`swift/rules.bzl`](../../../swift/rules.bzl)) | |
| 48 | +| a wrapper used across languages | `misc/bazel/` | |
| 49 | + |
| 50 | +Prefer inline rules in `BUILD.bazel`. A `.bzl` file earns its `load()` only when the shape repeats across packages or a |
| 51 | +value must be computed: [`rust.bzl`](../../../misc/bazel/rust.bzl) is worth it because every Rust binary in the repo |
| 52 | +must get the same universal-binary wrapper and symbols test, and forgetting either is a release bug. The `_gen_binaries` |
| 53 | +list in [`go/BUILD.bazel`](../../../go/BUILD.bazel) is not — it is shared by two targets in one file, so a local |
| 54 | +variable does the job. |
| 55 | + |
| 56 | +Macros here are typically a thin public wrapper around a private rule (`codeql_csharp_binary`, `swift_cc_binary`). Keep |
| 57 | +the rule narrow and the ergonomics in the macro. Generated targets are named after the macro's `name` (`<name>-all`, |
| 58 | +`single_arch/<name>`, `internal/<name>`) and kept private; when an error names a target you cannot find in any source |
| 59 | +file, a macro minted it — grep the suffix under `misc/bazel/`. |
| 60 | + |
| 61 | +## Shared helpers |
| 62 | + |
| 63 | +Frequently-used pieces, so you load the existing one instead of rewriting it. Read the file for its actual exports. |
| 64 | + |
| 65 | +| `load()` path | Covers | |
| 66 | +| --- | --- | |
| 67 | +| `//misc/bazel:pkg.bzl` | CodeQL packs and packaging | |
| 68 | +| `//misc/bazel:os.bzl` | platform and architecture selection | |
| 69 | +| `//misc/bazel:lfs.bzl` | on-demand git-LFS repositories | |
| 70 | +| `//misc/bazel:rust.bzl` | Rust binary wrapper | |
| 71 | +| `//misc/bazel:csharp.bzl` | C# binary/library/test wrappers | |
| 72 | +| `//misc/bazel:utils.bzl` | `select_os`; prefer `os.bzl`'s `os_select` in new code | |
| 73 | + |
| 74 | +[`defs.bzl`](../../../defs.bzl) at the root exports `codeql_platform` for *dependent* modules — nothing in this repo |
| 75 | +loads it, and it is not the way to get the platform string here (use `os.bzl`). |
| 76 | + |
| 77 | +## Platform selection |
| 78 | + |
| 79 | +`codeql_platform_select` discriminates the four platforms CodeQL knows about: `linux64`, `linux_arm64`, `osx64`, |
| 80 | +`win64`. `otherwise` supplies the value for whichever of those four you leave unset — it is **not** a |
| 81 | +`//conditions:default`. **There is deliberately no fallback from `linux_arm64` to `linux64`**; if you only care about |
| 82 | +the OS, use `os_select`, which gives Linux the same value on both architectures and has a `posix` shorthand for the |
| 83 | +shared Linux/macOS value. |
| 84 | + |
| 85 | +In a macro (no `ctx`) it returns a `select()`: |
| 86 | + |
| 87 | +```python |
| 88 | +load("//misc/bazel:os.bzl", "codeql_platform_select") |
| 89 | +load("//misc/bazel:pkg.bzl", "codeql_pkg_files") |
| 90 | + |
| 91 | +codeql_pkg_files( |
| 92 | + name = "extractor-arch", |
| 93 | + exes = codeql_platform_select( |
| 94 | + otherwise = ["//unified/extractor"], |
| 95 | + win64 = ["//unified/extractor-unsupported-os:extractor"], |
| 96 | + ), |
| 97 | + prefix = "tools/{CODEQL_PLATFORM}", |
| 98 | +) |
| 99 | +``` |
| 100 | + |
| 101 | +If implementation code needs to *branch* on the value rather than pass it through, pass `ctx` and add |
| 102 | +`OS_DETECTION_ATTRS` to the rule's attributes — the value is then resolved eagerly instead of being an opaque `select()`: |
| 103 | + |
| 104 | +```python |
| 105 | +load("//misc/bazel:os.bzl", "OS_DETECTION_ATTRS", "os_select") |
| 106 | + |
| 107 | +def _impl(ctx): |
| 108 | + ext = os_select(ctx, windows = ".exe", posix = "") |
| 109 | + ... |
| 110 | + |
| 111 | +my_rule = rule( |
| 112 | + implementation = _impl, |
| 113 | + attrs = {"src": attr.label()} | OS_DETECTION_ATTRS, |
| 114 | +) |
| 115 | +``` |
| 116 | + |
| 117 | +## Packs |
| 118 | + |
| 119 | +A pack is the unit that becomes an extractor pack. See [`unified/BUILD.bazel`](../../../unified/BUILD.bazel) for a |
| 120 | +minimal complete example and [`pkg.bzl`](../../../misc/bazel/pkg.bzl) for the arguments. The non-obvious parts: |
| 121 | + |
| 122 | +* **`{CODEQL_PLATFORM}` in a destination path is the routing mechanism**, not just a substitution. A path containing it |
| 123 | + is *arch-specific* and lands in the per-architecture zip; every other path is *common*. So `prefix = |
| 124 | + "tools/{CODEQL_PLATFORM}"` both places the file and marks it arch-specific. `arch_overrides` forces named |
| 125 | + destinations into the arch-specific part without a placeholder. |
| 126 | +* **`codeql_pkg_files` splits `srcs` (plain) from `exes` (mode 755)** and **rejects `attributes =`** with an explicit |
| 127 | + error — use `exes` rather than hand-rolling `pkg_attributes(mode = "755")`. |
| 128 | +* **`pkg_dirs` and `pkg_symlinks` are unsupported** and fail at analysis time. |
| 129 | +* `codeql_pack` also generates an installer and an `install` alias, hence `bazel run //unified:install`. Pass |
| 130 | + `installer_alias = None` if one package defines several packs. |
| 131 | +* `codeql_pack_group` exists for bundling packs into distribution zips, but nothing in this repo instantiates it. |
| 132 | + |
| 133 | +## Dependencies and `MODULE.bazel` |
| 134 | + |
| 135 | +In order of preference: |
| 136 | + |
| 137 | +1. **A [Bazel Central Registry](https://registry.bazel.build/) module** — a `bazel_dep` in |
| 138 | + [`MODULE.bazel`](../../../MODULE.bazel). |
| 139 | +2. **A patched upstream module** — add it under |
| 140 | + [`misc/bazel/registry`](../../../misc/bazel/registry), which `.bazelrc` puts ahead of the BCR. Put patches in |
| 141 | + `modules/<repo>/<version>/patches`, rename the version with a `-codeql.N` suffix, and run |
| 142 | + [`fix.py`](../../../misc/bazel/registry/fix.py) to realign the metadata. |
| 143 | +3. **A raw archive** — `http_archive` via `use_repo_rule`, or a repository rule. Copy an adjacent declaration and keep |
| 144 | + its checksum field populated. |
| 145 | + |
| 146 | +Vendored Rust crates under [`misc/bazel/3rdparty`](../../../misc/bazel/3rdparty) are generated — regenerate with |
| 147 | +`update_cargo_deps.sh` rather than editing, and keep the `use_repo` lists in sync (`bazel mod tidy` handles several). |
| 148 | + |
| 149 | +## Building standalone |
| 150 | + |
| 151 | +[`MODULE.bazel`](../../../MODULE.bazel) declares `semmle_code` with a `local_path_override` pointing at `..`, which |
| 152 | +resolves when this repo is checked out inside the internal module. [`.bazelrc`](../../../.bazelrc) — read when Bazel is |
| 153 | +invoked in *this* workspace — overrides that with a stub, and this line is the whole reason a standalone build resolves: |
| 154 | + |
| 155 | +``` |
| 156 | +common --override_module=semmle_code=%workspace%/misc/bazel/semmle_code_stub |
| 157 | +``` |
| 158 | + |
| 159 | +Do not change either the override path or the `local_path_override`; they work as a pair. |
| 160 | + |
| 161 | +[`misc/bazel/semmle_code_stub`](../../../misc/bazel/semmle_code_stub) is an otherwise empty module supplying no-op |
| 162 | +versions of the internal helpers that shared `.bzl` files load *unconditionally*. That is its only job, and it is small |
| 163 | +enough to read. |
| 164 | + |
| 165 | +**Extend the stub only when a `.bzl` file every standalone target loads gains a new internal `load()`** — that breaks |
| 166 | +package loading outright, for everyone. Prefer not needing one. **Never add a stub so that an internal-only target |
| 167 | +appears to build**: some targets depend on internal libraries (`grep -rl @semmle_code --include=*.bazel` finds them) and |
| 168 | +are correctly unbuildable here. Unlike a `load()`, such a dependency only fails when that target is actually requested. |
| 169 | + |
| 170 | +[`.bazelrc.internal`](../../../.bazelrc.internal) is **not** read here; it carries settings for the internal build. A |
| 171 | +setting needed by both has to be written in both files, with paths differing because this repo sits at a different depth |
| 172 | +there. |
0 commit comments