diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d64877..099f647 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,13 +58,54 @@ jobs: run: cargo hack clippy --feature-powerset -- -D warnings - name: Test - run: cargo hack test --feature-powerset + # `bundled` must stay enabled for every combination: it is what compiles + # and statically links the C++ ada sources. Without it (and with no + # external ada provided via ADA_LIB_DIR/ADA_LIB_NAME) the test binaries + # fail to link with `undefined symbol: ada_*`. `--features bundled` forces + # it into each powerset entry while still exercising the other features. + run: cargo hack test --feature-powerset --features bundled - name: Check Documentation env: RUSTDOCFLAGS: '-D warnings' run: cargo hack doc --feature-powerset + external-ada: + name: External ada (non-bundled) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: external-ada + save-if: ${{ github.ref_name == 'main' }} + + - run: rustup show + + # Exercise the non-bundled linking path: instead of compiling the bundled + # C++ sources, build ada once as a standalone shared library and link the + # crate against it via ADA_LIB_DIR / ADA_LIB_NAME. This mirrors how a host + # build system (Bazel, CMake, a distro package) would provide ada. The + # shared library records its own libstdc++ dependency (DT_NEEDED), so the + # final Rust binary needs no extra C++ runtime link flags. + - name: Build standalone libada.so + run: | + c++ -std=c++20 -O2 -DADA_INCLUDE_URL_PATTERN=0 -Ideps -fPIC -shared \ + deps/ada.cpp -o "$RUNNER_TEMP/libada.so" + + # `bundled` is intentionally disabled (default-features off). `std` is + # re-enabled because dropping default features also drops it. The serde + # combination is run separately to cover the extra feature on this path. + - name: Test against external ada + env: + ADA_LIB_DIR: ${{ runner.temp }} + ADA_LIB_NAME: ada + LD_LIBRARY_PATH: ${{ runner.temp }} + run: | + cargo test --no-default-features --features std + cargo test --no-default-features --features serde + sanitizers: name: AddressSanitizer + LeakSanitizer runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 78d85d3..dbb517a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,11 @@ path = "bench/wpt.rs" harness = false [features] -default = ["std"] +default = ["bundled", "std"] +# compile and statically link the bundled C++ `ada` sources. Enabled by +# default; disable (`default-features = false`) to link an externally provided +# ada instead (see `ADA_LIB_DIR` / `ADA_LIB_NAME` in build.rs). +bundled = [] # pass `cpp_set_stdlib("c++")` to `cc` libcpp = [] # enables serde serialization/deserialization support diff --git a/README.md b/README.md index 2b5e2ba..eaa21ff 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,44 @@ Enabling this feature and `std` would provide you both `Serialize` and `Deserial **libcpp:** Build `ada-url` with `libc++`. This feature is disabled by default. Enabling this feature without `libc++` installed would cause compile error. +**bundled:** Compile and statically link the bundled C++ `ada` sources. This +feature is **enabled by default**. See "Linking against an external ada" below +for how to disable it. + +### Linking against an external ada + +By default (the `bundled` feature) the build script compiles the bundled C++ +`ada` sources and links them statically. If ada is already provided by your +host build system (e.g. Bazel, CMake, or a distribution package), compiling the +bundled copy would produce a second, duplicate copy of ada in the final binary. + +Disable the `bundled` feature to skip the bundled build and link against an +external ada instead: + +```toml +# Cargo.toml — re-enable `std` since disabling default features also drops it +ada-url = { version = "3", default-features = false, features = ["std"] } +``` + +When `bundled` is disabled, these optional environment variables configure how +the external ada is linked: + +| Variable | Description | +| ------------- | -------------------------------------------------------------------------------------------------- | +| `ADA_LIB_DIR` | Optional directory added to the linker search path (`cargo:rustc-link-search=native`). | +| `ADA_LIB_NAME`| Optional library to link, passed verbatim to `cargo:rustc-link-lib` (e.g. `ada` or `static=ada`). | + +If neither is provided, no link directive is emitted, leaving it to the host +build system to provide the ada symbols at final link time. The external ada +must expose the same C ABI (`ada_*`) and be ABI-compatible with this crate's +version. + +```sh +# Link against a system-installed libada in /usr/local/lib +ADA_LIB_DIR=/usr/local/lib ADA_LIB_NAME=ada \ + cargo build --no-default-features --features std +``` + ### Performance Ada is fast. The benchmark below shows **3.49 times** faster URL parsing compared to `url` diff --git a/build.rs b/build.rs index 746695e..88309ac 100644 --- a/build.rs +++ b/build.rs @@ -88,6 +88,42 @@ fn ndk_major_version(ndk_dir: &Path) -> u32 { } fn main() { + // By default the bundled C++ `ada` sources are compiled and linked + // statically (the `bundled` feature, enabled by default). Disabling the + // `bundled` feature (e.g. `default-features = false`) skips that compile and + // instead links against an externally provided ada library. This is useful + // when ada is already provided by the host build system (e.g. Bazel, CMake, + // or a distro package), where compiling the bundled copy would otherwise + // produce a second, duplicate copy of ada in the final binary. + // + // When the `bundled` feature is disabled, these optional environment + // variables configure how the external ada is linked: + // + // ADA_LIB_DIR directory added to the link search path. + // ADA_LIB_NAME library to link, passed verbatim to `cargo:rustc-link-lib` + // (e.g. `ada` or `static=ada`). + // + // If neither is provided, no link directive is emitted, leaving it to the + // host build system to provide the ada symbols at final link time. The + // external ada must expose the same C ABI (`ada_*`) and be ABI-compatible + // with this crate's version. + println!("cargo:rerun-if-env-changed=ADA_LIB_DIR"); + println!("cargo:rerun-if-env-changed=ADA_LIB_NAME"); + if !cfg!(feature = "bundled") { + if let Some(dir) = env::var_os("ADA_LIB_DIR") { + println!( + "cargo:rustc-link-search=native={}", + Path::new(&dir).display() + ); + } + if let Ok(name) = env::var("ADA_LIB_NAME") + && !name.is_empty() + { + println!("cargo:rustc-link-lib={name}"); + } + return; + } + let target_str = env::var("TARGET").unwrap(); let target: Vec = target_str.split('-').map(|s| s.into()).collect(); assert!(target.len() >= 2, "Failed to parse TARGET {}", target_str);