unified: Make Bazel the only supported build

Nothing builds this extractor with `cargo` any more: the tests, the extractor
pack and the corpus regeneration all go through Bazel. The `cargo` half of the
build is now dead weight, and it is not free -- it is a second, independent way
of compiling the same Swift code, kept in step by hand.

`build.rs` shelled out to `swift build` on the SwiftPM package in `swift/`,
scraped `swiftc -print-target-info` for the runtime directory, and emitted the
resulting link and rpath flags. Bazel does none of that: it compiles
`SwiftSyntaxFFI.swift` directly against `@swift-syntax`, resolved from the Bazel
Central Registry. `Package.swift` and `Package.resolved` existed only for that
`swift build`.

Deleting them collapses three version pins into one. `MODULE.bazel` pinned the
toolchain, `.swift-version` pinned it again for `swiftly` and `build.rs`, and
`Package.swift` pinned the `swift-syntax` release a second time -- with a
comment asking that they be kept in sync and nothing enforcing it. Two Swift
builds that could silently diverge become one that cannot.

`cargo check` still works, since checking does not link, so rust-analyzer keeps
working across the crate -- completion, go-to-definition and inline diagnostics
included. What no longer works is `cargo build`/`cargo test`, and hence the
editor's "run test" lens; use `bazel test //unified/extractor:all_tests`. The
Cargo manifests stay because `misc/bazel/3rdparty` resolves third-party crate
versions from them.

`scripts/create-extractor-pack.sh` becomes a wrapper around the `codeql_pack`
installer that already existed, rather than a parallel cargo-based assembly of
the same pack.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 297ff885-7c13-4202-a9d0-bc0b17f68d8e
This commit is contained in:
Taus
2026-07-30 15:07:17 +00:00
parent 6dc61eb56f
commit 2aba5f1362
9 changed files with 36 additions and 245 deletions

View File

@@ -228,13 +228,8 @@ use_repo(
# auto-registers `xcode_swift_toolchain` (host Xcode + OS-provided Swift
# runtime), which is not hermetic.
#
# The version is pinned as a literal rather than read from
# `unified/swift-syntax-rs/.swift-version` via `swift_version_file`: the latter
# makes the extension `module_ctx.read` a `//unified/...` label, which fails to
# resolve when this repo is consumed as a dependency module (`@@ql+`) whose
# `unified/swift-syntax-rs` package is not loadable in that context. Keep this
# in sync with `unified/swift-syntax-rs/.swift-version` (used by the `cargo`
# build) and the `swift-syntax` release in `swift/Package.swift`.
# This and the `swift-syntax` version above are the only pins: the extractor is
# built solely by Bazel, so there is no second Swift build to keep in step.
swift = use_extension("@rules_swift//swift:extensions.bzl", "swift")
swift.toolchain(
name = "swift_toolchain",

View File

@@ -4,9 +4,10 @@ This is a CodeQL extractor that maps a language's parse tree onto a shared AST
using the `yeast` desugaring engine. Swift, the only language so far, is parsed
by Apple's swift-syntax rather than by tree-sitter.
Build and test with Bazel, whose Swift toolchain is hermetic on Linux, so
nothing needs to be installed locally. The extractor links `swift-syntax`, so a
`cargo` build additionally needs a local Swift toolchain.
Everything is built and tested with Bazel, whose Swift toolchain is hermetic on
Linux, so nothing needs to be installed locally. `cargo build`/`cargo test` do
not work here: the extractor links a Swift FFI shim that only Bazel builds.
(`cargo check` still works, so rust-analyzer is unaffected.)
## Building
- To build the extractor pack, run `scripts/create-extractor-pack.sh`.

View File

@@ -1,2 +1 @@
/target
/swift/.build

View File

@@ -1 +0,0 @@
6.3.2

View File

@@ -1,3 +1,6 @@
# `BUILD.bazel` is the build. This file feeds `misc/bazel/3rdparty`, which
# resolves third-party crate versions from it, and `cargo check`; `cargo
# build`/`test` cannot link the Swift shim.
[package]
name = "swift-syntax-rs"
description = "Rust wrapper around the swift-syntax package for parsing Swift source"

View File

@@ -4,8 +4,11 @@ A Rust wrapper around the [swift-syntax](https://github.com/swiftlang/swift-synt
package, allowing Swift source code to be parsed from Rust.
Parsing is delegated to a small Swift shim (in [`swift/`](swift/)) that links
against `SwiftSyntax`/`SwiftParser` and exposes a tiny C ABI. The Rust crate
builds that shim (via `build.rs`) and provides safe bindings on top of it.
against `SwiftSyntax`/`SwiftParser` and exposes a tiny C ABI. This crate provides
safe bindings on top of it.
Bazel builds both halves; there is no `cargo` build (see
[Building & testing](#building--testing)).
## Output format
@@ -122,47 +125,12 @@ an unparenthesised `a *** b + c` is a single flat sequence whose structure
cannot be determined without knowing `***`'s precedence, so it is left flat in
its entirety.
## Prerequisites
The build does not depend on any particular version manager. You need:
- **Rust** — pinned to `1.88` by the repo-root [`rust-toolchain.toml`](../../rust-toolchain.toml),
which `rustup` picks up automatically.
- **Swift** — pinned to the version in [`.swift-version`](.swift-version)
(currently `6.3.2`), used to build `swift-syntax` `603.0.2`. Install it any way
you like — [swift.org](https://www.swift.org/install/) or
[swiftly](https://www.swift.org/swiftly/) (which reads `.swift-version`), or a
system package. Just make sure `swift` is on your `PATH` (or point `build.rs`
at it with the `SWIFT` environment variable).
On Debian/Ubuntu the Swift runtime also needs `libncurses6` (and related libs)
available on the system.
## Building & testing
With `cargo` and `swift` on `PATH`:
```sh
cargo build
cargo test
```
If your `swift`/`swiftc` are not on `PATH`, point the build at them explicitly:
```sh
SWIFT=/path/to/swift SWIFTC=/path/to/swiftc cargo build
```
The first build compiles `swift-syntax` and can take several minutes.
## Building with Bazel (CI)
CI builds this crate hermetically with Bazel. A Swift toolchain is downloaded
from swift.org by the official `rules_swift` standalone toolchain extension
(wired up in the repo-root `MODULE.bazel`), `swift-syntax` is pulled from the
Bazel Central Registry, and the FFI shim is compiled as a `swift_library` that
the Rust targets link against. `build.rs` is not used under Bazel; it only
builds the Swift shim for the local `cargo` workflow.
Everything is built by Bazel, which downloads a Swift toolchain from swift.org
via the official `rules_swift` standalone toolchain extension (wired up in the
repo-root `MODULE.bazel`) and pulls `swift-syntax` from the Bazel Central
Registry. Nothing has to be installed locally on Linux:
```sh
bazel build //unified/swift-syntax-rs:swift-syntax-parse
@@ -170,6 +138,13 @@ bazel test //unified/swift-syntax-rs:swift_syntax_rs_test
bazel run //unified/swift-syntax-rs:swift-syntax-parse < some.swift
```
The first build compiles `swift-syntax` and can take several minutes.
`cargo build`/`cargo test` do **not** work: the Swift shim is compiled by a
`swift_library` in [`BUILD.bazel`](BUILD.bazel), so a `cargo` link finds no
`ssr_*` symbols. `cargo check` does work — it does not link — which is all
rust-analyzer needs.
Requirements:
- **`clang`** must be installed on the runner. `rules_swift` requires the Bazel
@@ -186,20 +161,12 @@ Requirements:
[`xcode_transition.bzl`](xcode_transition.bzl)), so other targets on macOS
keep using Bazel's default CC toolchain.
The Swift compiler version is kept in sync across three places: the
[`.swift-version`](.swift-version) file (read by the local `cargo`/`swift build`
and by [swiftly](https://www.swift.org/swiftly/)), the literal `swift_version`
pinned on `swift.toolchain(...)` in the root `MODULE.bazel` (the hermetic
swift.org **Linux** Bazel toolchain), and the `swift-syntax` release in
`swift/Package.swift`. On **macOS** the version is *not* pinned by the Bazel
build: `rules_swift` auto-registers the host `xcode_swift_toolchain`, which uses
whichever Swift ships with the installed Xcode. So the pin governs Linux (and
local) builds, while the macOS compiler version depends on the host Xcode.
(The Bazel toolchain pins a literal rather than reading `.swift-version` via
`swift_version_file`, because the latter makes the module extension read a
`//unified/...` label, which fails when this repo is consumed as a dependency
module.)
Versions are pinned in the root `MODULE.bazel` and nowhere else: the
`swift_version` literal on `swift.toolchain(...)` selects the hermetic swift.org
**Linux** toolchain, and `bazel_dep(name = "swift-syntax", ...)` selects the
`swift-syntax` release. On **macOS** the compiler version is *not* pinned:
`rules_swift` auto-registers the host `xcode_swift_toolchain`, so it follows
whichever Swift ships with the installed Xcode.
## Usage
@@ -213,7 +180,7 @@ println!("{json}");
CLI (reads a file argument or stdin, prints the syntax tree as JSON):
```sh
echo 'let x = 1' | cargo run --bin swift-syntax-parse
echo 'let x = 1' | bazel run //unified/swift-syntax-rs:swift-syntax-parse
```
## Converting to a yeast AST
@@ -221,14 +188,12 @@ echo 'let x = 1' | cargo run --bin swift-syntax-parse
The JSON tree is consumed by the CodeQL extractor, which converts it into a
[`yeast::Ast`](../../shared/yeast) — the in-memory format its rewrite rules
operate on. That adapter is a pure-Rust module living in the extractor
(`unified/extractor/src/languages/swift/adapter.rs`), so the extractor never
needs the Swift toolchain: it consumes the JSON produced out-of-process by this
crate's `parse_to_json` / the `swift-syntax-parse` binary.
(`unified/extractor/src/languages/swift/adapter.rs`); the extractor links this
crate and calls `parse_to_json` in-process.
## Layout
- `swift/` — Swift package exposing the `ssr_parse_json` / `ssr_string_free` C ABI.
- `build.rs` — builds the Swift package and emits link/rpath flags (local `cargo` only).
- `BUILD.bazel` — Bazel targets for the hermetic CI build (swift_library + rust targets).
- `swift/` — Swift sources exposing the `ssr_parse_json` / `ssr_string_free` C ABI.
- `BUILD.bazel` the build (swift_library + rust targets).
- `src/lib.rs` — safe Rust bindings (`parse_to_json`).
- `src/main.rs` — demo CLI.

View File

@@ -1,120 +0,0 @@
use std::env;
use std::path::PathBuf;
use std::process::Command;
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let swift_dir = manifest_dir.join("swift");
// Emitting any `rerun-if-changed` disables Cargo's default whole-package
// scan, so we must list every input to the `swift build` below. Watch the
// package manifests, the pinned dependency lockfile, the pinned compiler
// version, and the entire Swift sources tree (a directory is scanned
// recursively). `.build/` is intentionally *not* watched (it is this
// build's output; watching it would cause perpetual rebuilds).
for input in [
swift_dir.join("Package.swift"),
swift_dir.join("Package.resolved"),
swift_dir.join("Sources"),
manifest_dir.join(".swift-version"),
] {
println!("cargo:rerun-if-changed={}", input.display());
}
println!("cargo:rerun-if-env-changed=SWIFT");
println!("cargo:rerun-if-env-changed=SWIFTC");
// Build the Swift FFI package as a release dynamic library.
//
// Degrade gracefully when there is no runnable Swift toolchain. This crate
// is a workspace member, so a plain `cargo check`/`fmt`/`clippy` at the repo
// root runs this build script; if `swift build` cannot even be spawned we
// emit a warning and skip the link directives rather than panicking, so
// those Swift-free workflows keep working. Only `cargo build`/`cargo test`
// then fail — at link time, which is fair: they genuinely need Swift (and CI
// builds go through Bazel anyway). A Swift toolchain that *is* present but
// whose build fails is still surfaced as a hard error below.
let mut command = Command::new(swift_bin());
command
.args(["build", "-c", "release"])
.current_dir(&swift_dir);
apply_bare_repository_workaround(&mut command);
let status = match command.status() {
Ok(status) => status,
Err(e) => {
println!(
"cargo:warning=skipping the Swift FFI build: failed to run `{swift} build`: {e}. \
Install a Swift toolchain (see https://www.swift.org/install/, e.g. via swiftly) \
and ensure `swift` is on PATH, or set the `SWIFT` environment variable, to build \
or test this crate. `cargo check`/`fmt`/`clippy` work without it. The pinned \
version is in `.swift-version`.",
swift = swift_bin(),
);
return;
}
};
assert!(status.success(), "`swift build` failed");
// Link against the freshly built dynamic library.
let build_dir = swift_dir.join(".build/release");
println!("cargo:rustc-link-search=native={}", build_dir.display());
println!("cargo:rustc-link-lib=dylib=SwiftSyntaxFFI");
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", build_dir.display());
// The executable also needs to find the Swift runtime libraries at run time.
if let Some(runtime) = swift_runtime_dir() {
println!("cargo:rustc-link-search=native={}", runtime.display());
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", runtime.display());
}
}
/// Query the active Swift toolchain for the directory containing its runtime
/// shared libraries (e.g. `libswiftCore.so`).
fn swift_runtime_dir() -> Option<PathBuf> {
let output = Command::new(swiftc_bin())
.arg("-print-target-info")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let info = String::from_utf8_lossy(&output.stdout);
// Extract the value of `"runtimeResourcePath": "..."` without pulling in a
// JSON dependency.
let key = "\"runtimeResourcePath\"";
let start = info.find(key)?;
let rest = &info[start + key.len()..];
let colon = rest.find(':')?;
let after = &rest[colon + 1..];
let open = after.find('"')?;
let value_start = &after[open + 1..];
let close = value_start.find('"')?;
let resource_path = &value_start[..close];
Some(PathBuf::from(resource_path).join(if cfg!(target_os = "macos") { "macosx" } else { "linux" }))
}
/// The `swift` driver to invoke: `$SWIFT` if set, otherwise `swift` from `PATH`.
/// This keeps the build tool-agnostic — any Swift install works; no particular
/// version manager is required.
fn swift_bin() -> String {
env::var("SWIFT").unwrap_or_else(|_| "swift".to_string())
}
/// The `swiftc` compiler to invoke: `$SWIFTC` if set, otherwise `swiftc` from
/// `PATH`.
fn swiftc_bin() -> String {
env::var("SWIFTC").unwrap_or_else(|_| "swiftc".to_string())
}
/// Some environments (notably GitHub Codespaces) inject
/// `GIT_CONFIG_KEY_0=safe.bareRepository` / `GIT_CONFIG_VALUE_0=explicit`, which
/// breaks the cached bare git repositories `swift build` uses. When exactly that
/// key is present, relax it to `all` for the `swift build` subprocess only
/// (rather than unconditionally, which could clobber an unrelated
/// `GIT_CONFIG_VALUE_0`).
fn apply_bare_repository_workaround(command: &mut Command) {
if env::var("GIT_CONFIG_KEY_0").as_deref() == Ok("safe.bareRepository") {
command.env("GIT_CONFIG_VALUE_0", "all");
}
}

View File

@@ -1,15 +0,0 @@
{
"originHash" : "169957f8fbd882866f8f9f1c68cde813dca3560577939a8ffec366e3d77548e4",
"pins" : [
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax.git",
"state" : {
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1",
"version" : "603.0.2"
}
}
],
"version" : 3
}

View File

@@ -1,36 +0,0 @@
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "SwiftSyntaxFFI",
platforms: [
// swift-syntax 603 requires macOS 10.15; declare it explicitly
// rather than relying on the swift-tools-version default (10.13).
.macOS(.v10_15),
],
products: [
// Dynamic library so the produced .so records its dependency on the
// Swift runtime libraries, keeping the Rust link step simple.
.library(
name: "SwiftSyntaxFFI",
type: .dynamic,
targets: ["SwiftSyntaxFFI"]
)
],
dependencies: [
.package(
url: "https://github.com/swiftlang/swift-syntax.git",
exact: "603.0.2"
)
],
targets: [
.target(
name: "SwiftSyntaxFFI",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "SwiftOperators", package: "swift-syntax"),
]
)
]
)