From df61748899ca58162d85f17565729673327af08e Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Thu, 1 Oct 2026 19:46:18 -0700 Subject: [PATCH 1/3] Bundle icebug + algo extension, test GDS_PAGE_RANK Ship PageRank (and other GDS functions) as Cypher UDFs by bundling the icebug 13.2 prebuilt and the Ladybug algo extension with the app: - scripts/download_icebug.sh: fetch platform prebuilt into ./icebug/ - scripts/download-liblbug.sh: fetch shared liblbug (algo extension dlopens it; static liblbug.a gets dead-stripped so LOAD EXTENSION fails with missing GDSFunction symbols) - .cargo/config.toml: ICEBUG_DIR + rpaths covering dist, dev, .app, and --target triple-dir test binary layouts - build.rs: -rdynamic for LOAD EXTENSION (ELF), icebug presence check, Windows networkit_state link - Cargo: icebug 13.2 behind default icebug-analytics feature, arrow 55->56 to match; tempfile dev-dep for the new test - src/backend.rs: auto-LOAD the bundled algo extension per connection (ExtensionManager is per-Database; BUGSCOPE_ALGO_EXTENSION override) + in-process graphr_page_rank cross-check - scripts/vendor_arrow.sh + stage_macos_bundle.sh: vendor libarrow/ libomp next to libnetworkit so the downloaded extension's @rpath/libarrow resolves; bundle dylib/.so/.dll in every archive - tests/gds_page_rank.rs: INSTALL/LOAD the prebuilt algo extension and assert GDS_PAGE_RANK works (skips only when offline) - README: document the new analytics path; .gitignore the locally built extension binary (dev artifact, not shipped) --- .cargo/config.toml | 83 +++++++++++ .gitignore | 4 + Cargo.lock | 271 ++++++++++++++++++++++++++++------ Cargo.toml | 10 +- README.md | 30 ++++ build.rs | 91 ++++++++++++ scripts/download-liblbug.sh | 105 +++++++++++++ scripts/download_icebug.sh | 94 ++++++++++++ scripts/stage_macos_bundle.sh | 94 ++++++++++++ scripts/vendor_arrow.sh | 50 +++++++ src/backend.rs | 124 +++++++++++++++- tests/gds_page_rank.rs | 142 ++++++++++++++++++ 12 files changed, 1049 insertions(+), 49 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 build.rs create mode 100755 scripts/download-liblbug.sh create mode 100755 scripts/download_icebug.sh create mode 100755 scripts/stage_macos_bundle.sh create mode 100755 scripts/vendor_arrow.sh create mode 100644 tests/gds_page_rank.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..a088098 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,83 @@ +# Use the prebuilt SHARED liblbug from ./liblbug (see scripts/download-liblbug.sh). +# +# Shared (not static) is required: the dlopen'd algo extension resolves +# ladybug symbols (e.g. GDSFunction::getLogicalPlan) from the host process at +# LOAD EXTENSION time. With a static liblbug.a, dead-code stripping drops +# unreferenced symbols and the extension fails to load. Mirrors +# ../bugscope/src-tauri/.cargo/config.toml. +# +# The icebug Rust crate's build script links libnetworkit from ./icebug +# (see scripts/download_icebug.sh). +[env] +LBUG_LIBRARY_DIR = { value = "liblbug", relative = true } +LBUG_INCLUDE_DIR = { value = "liblbug", relative = true } +LBUG_SHARED = "1" +ICEBUG_DIR = { value = "icebug", relative = true } + +# Embed rpath so the binary finds the bundled shared libs at runtime: +# - dist archives ship `liblbug/` + `icebug/lib/` next to the binary +# ($ORIGIN / @executable_path entries) +# - macOS .app bundles stage them under Contents/Frameworks +# - cargo dev/test binaries resolve ../../../ entries +# (target/{debug,release}/deps/ -> repo root) +[target.aarch64-apple-darwin] +rustflags = [ + "-C", "link-arg=-Wl,-rpath,@executable_path", + "-C", "link-arg=-Wl,-rpath,@executable_path/icebug/lib", + "-C", "link-arg=-Wl,-rpath,@executable_path/liblbug", + "-C", "link-arg=-Wl,-rpath,@executable_path/../Frameworks", + "-C", "link-arg=-Wl,-rpath,@executable_path/../icebug/lib", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../liblbug", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../icebug/lib", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../../liblbug", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../../../liblbug", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../../icebug/lib", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../../../icebug/lib", +] + +[target.x86_64-apple-darwin] +rustflags = [ + "-C", "link-arg=-Wl,-rpath,@executable_path", + "-C", "link-arg=-Wl,-rpath,@executable_path/icebug/lib", + "-C", "link-arg=-Wl,-rpath,@executable_path/liblbug", + "-C", "link-arg=-Wl,-rpath,@executable_path/../Frameworks", + "-C", "link-arg=-Wl,-rpath,@executable_path/../icebug/lib", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../liblbug", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../icebug/lib", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../../liblbug", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../../../liblbug", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../../icebug/lib", + "-C", "link-arg=-Wl,-rpath,@executable_path/../../../../icebug/lib", +] + +[target.aarch64-unknown-linux-gnu] +rustflags = [ + "-C", "link-arg=-Wl,-rpath,$ORIGIN", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/icebug/lib", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/liblbug", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../icebug/lib", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../liblbug", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../icebug/lib", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../../liblbug", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../../../liblbug", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../../icebug/lib", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../../../icebug/lib", + # RPATH (not RUNPATH) so transitive deps like libarrow (needed by + # libnetworkit.so) also resolve from these dirs. + "-C", "link-arg=-Wl,--disable-new-dtags", +] + +[target.x86_64-unknown-linux-gnu] +rustflags = [ + "-C", "link-arg=-Wl,-rpath,$ORIGIN", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/icebug/lib", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/liblbug", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../icebug/lib", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../liblbug", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../icebug/lib", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../../liblbug", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../../../liblbug", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../../icebug/lib", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/../../../../icebug/lib", + "-C", "link-arg=-Wl,--disable-new-dtags", +] diff --git a/.gitignore b/.gitignore index 8d8b44c..4816625 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ /target/ *.lbdb +/icebug/ +/liblbug/ +# Locally built algo extension (dev only; CI/app use INSTALL from the official repo). +/extensions/ diff --git a/Cargo.lock b/Cargo.lock index 102e3fa..cf76beb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,16 +126,34 @@ version = "55.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f15b4c6b148206ff3a2b35002e08929c2462467b62b9c02036d9c34f9ef994" dependencies = [ - "arrow-arith", - "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", - "arrow-ord", - "arrow-row", - "arrow-schema", - "arrow-select", - "arrow-string", + "arrow-arith 55.2.0", + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-cast 55.2.0", + "arrow-data 55.2.0", + "arrow-ord 55.2.0", + "arrow-row 55.2.0", + "arrow-schema 55.2.0", + "arrow-select 55.2.0", + "arrow-string 55.2.0", +] + +[[package]] +name = "arrow" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb98341a7e051bb79731ecb33ec00cbd6e0e315a542d6732b46d462c9215ea2" +dependencies = [ + "arrow-arith 56.2.1", + "arrow-array 56.2.1", + "arrow-buffer 56.2.1", + "arrow-cast 56.2.1", + "arrow-data 56.2.1", + "arrow-ord 56.2.1", + "arrow-row 56.2.1", + "arrow-schema 56.2.1", + "arrow-select 56.2.1", + "arrow-string 56.2.1", ] [[package]] @@ -144,10 +162,24 @@ version = "55.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30feb679425110209ae35c3fbf82404a39a4c0436bb3ec36164d8bffed2a4ce4" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "chrono", + "num", +] + +[[package]] +name = "arrow-arith" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce4751cbc4bcccfeeea79df9571ff1dc066d61e44723c7604d11c7937f5b560" +dependencies = [ + "arrow-array 56.2.1", + "arrow-buffer 56.2.1", + "arrow-data 56.2.1", + "arrow-schema 56.2.1", "chrono", "num", ] @@ -159,15 +191,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70732f04d285d49054a48b72c54f791bb3424abae92d27aafdf776c98af161c8" dependencies = [ "ahash", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", "chrono", "half", "hashbrown 0.15.5", "num", ] +[[package]] +name = "arrow-array" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02ccba2e977a3aabb4384036109ca32f552399a2bc0588f925f91ed073ce70c" +dependencies = [ + "ahash", + "arrow-buffer 56.2.1", + "arrow-data 56.2.1", + "arrow-schema 56.2.1", + "chrono", + "half", + "hashbrown 0.16.1", + "num", +] + [[package]] name = "arrow-buffer" version = "55.2.0" @@ -179,17 +227,48 @@ dependencies = [ "num", ] +[[package]] +name = "arrow-buffer" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90f8bece6a9ee316a699fbbfde368a206676a1206ce89b50f07937648e76c3c" +dependencies = [ + "bytes", + "half", + "num", +] + [[package]] name = "arrow-cast" version = "55.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4f12eccc3e1c05a766cafb31f6a60a46c2f8efec9b74c6e0648766d30686af8" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "arrow-select 55.2.0", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-cast" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ffe645cfb4e80b1ca37a3a106ce7b4af66ccdd60c655a57e6b9aab096164a7" +dependencies = [ + "arrow-array 56.2.1", + "arrow-buffer 56.2.1", + "arrow-data 56.2.1", + "arrow-schema 56.2.1", + "arrow-select 56.2.1", "atoi", "base64", "chrono", @@ -205,8 +284,20 @@ version = "55.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de1ce212d803199684b658fc4ba55fb2d7e87b213de5af415308d2fee3619c2" dependencies = [ - "arrow-buffer", - "arrow-schema", + "arrow-buffer 55.2.0", + "arrow-schema 55.2.0", + "half", + "num", +] + +[[package]] +name = "arrow-data" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78468c813909465dd0f858950c8a0614eb63608134acf95c602ec21381258b28" +dependencies = [ + "arrow-buffer 56.2.1", + "arrow-schema 56.2.1", "half", "num", ] @@ -217,11 +308,24 @@ version = "55.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6506e3a059e3be23023f587f79c82ef0bcf6d293587e3272d20f2d30b969b5a7" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "arrow-select 55.2.0", +] + +[[package]] +name = "arrow-ord" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aed58a38c3db0a2cf75ef70e3cb6bc4bd0da0a3d390de37c36139b31fae826e8" +dependencies = [ + "arrow-array 56.2.1", + "arrow-buffer 56.2.1", + "arrow-data 56.2.1", + "arrow-schema 56.2.1", + "arrow-select 56.2.1", ] [[package]] @@ -230,10 +334,23 @@ version = "55.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52bf7393166beaf79b4bed9bfdf19e97472af32ce5b6b48169d321518a08cae2" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "half", +] + +[[package]] +name = "arrow-row" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "079ced0517daf4f09b070d09ff641cee7cc331aa216bebcb25d1a6474ad53086" +dependencies = [ + "arrow-array 56.2.1", + "arrow-buffer 56.2.1", + "arrow-data 56.2.1", + "arrow-schema 56.2.1", "half", ] @@ -246,6 +363,15 @@ dependencies = [ "bitflags 2.13.2", ] +[[package]] +name = "arrow-schema" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0d5eb3fe25337ff83e8333a08379bdd1540b0961b1c888f6e505d971c198e1" +dependencies = [ + "bitflags 2.13.2", +] + [[package]] name = "arrow-select" version = "55.2.0" @@ -253,10 +379,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2b45757d6a2373faa3352d02ff5b54b098f5e21dccebc45a21806bc34501e5" dependencies = [ "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "num", +] + +[[package]] +name = "arrow-select" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2368a78bd32902dba39d52519d70f63799c8b5dc8a9477129a30c2fd3dc70c19" +dependencies = [ + "ahash", + "arrow-array 56.2.1", + "arrow-buffer 56.2.1", + "arrow-data 56.2.1", + "arrow-schema 56.2.1", "num", ] @@ -266,11 +406,28 @@ version = "55.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0377d532850babb4d927a06294314b316e23311503ed580ec6ce6a0158f49d40" dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "arrow-select", + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "arrow-select 55.2.0", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "arrow-string" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dece58a130b9187756ded8bc071bd8ee9dd7a146566af244b297c7e632fd1ef7" +dependencies = [ + "arrow-array 56.2.1", + "arrow-buffer 56.2.1", + "arrow-data 56.2.1", + "arrow-schema 56.2.1", + "arrow-select 56.2.1", "memchr", "num", "regex", @@ -848,11 +1005,13 @@ name = "bugscope" version = "0.1.0" dependencies = [ "anyhow", - "arrow", + "arrow 56.2.1", "gpui", + "icebug", "lbug", "serde", "serde_json", + "tempfile", ] [[package]] @@ -2698,6 +2857,12 @@ dependencies = [ "foldhash 0.1.5", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashbrown" version = "0.17.1" @@ -2902,6 +3067,20 @@ dependencies = [ "cc", ] +[[package]] +name = "icebug" +version = "13.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea211efd8077337fc0ee0353300fcd676cb6c64a86fd439126f6477827603c" +dependencies = [ + "arrow-array 56.2.1", + "arrow-buffer 56.2.1", + "cc", + "cxx", + "cxx-build", + "pkg-config", +] + [[package]] name = "icu_collections" version = "2.3.0" @@ -3226,11 +3405,11 @@ dependencies = [ [[package]] name = "lbug" -version = "0.20.4" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa792a6ec0595098bf41f2f6d243bb705a0d4c01bb94f44e63cc49988796b0e8" +checksum = "417db8d481578b1014953a1ce24508382bd791938bb974f5462f1621cd1cfd3d" dependencies = [ - "arrow", + "arrow 55.2.0", "cmake", "cxx", "cxx-build", diff --git a/Cargo.toml b/Cargo.toml index eb8fa23..e9e7186 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,18 @@ name = "bugscope" version = "0.1.0" edition = "2021" +[features] +default = ["icebug-analytics"] +icebug-analytics = ["dep:icebug"] + [dependencies] gpui = "0.2" lbug = { version = "0.21.1", features = ["arrow"] } -arrow = { version = "55", default-features = false, features = ["ffi"] } +icebug = { version = "=13.2.0", optional = true } +arrow = { version = "56", default-features = false, features = ["ffi"] } serde = { version = "1", features = ["derive"] } serde_json = "1" anyhow = "1" + +[dev-dependencies] +tempfile = "3" diff --git a/README.md b/README.md index dc3d175..c975aa5 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,36 @@ cargo run --release Working directory matters only for the initial sidebar scan: `*.lbdb` files under `.` are listed automatically. Anything else can be opened via **Open file…**, so you can run from anywhere. +## Graph analytics (icebug / GDS) + +Every connection runs `LOAD algo` (installing from the official repo at +`https://extension.ladybugdb.com` on first use), so PageRank and friends work +straight from the query box as Cypher table functions: + +```cypher +CALL PROJECT_GRAPH('G', ['N'], ['E']); +CALL GDS_PAGE_RANK('G') RETURN node.id, rank ORDER BY rank DESC; +``` + +Available: `PROJECT_GRAPH`, `PAGE_RANK`, `GDS_PAGE_RANK`, `GDS_LOUVAIN`, +`GDS_LEIDEN`, `GDS_PPR`, `GDS_NODE2VEC`, k-core, components, spanning forest. +Set `BUGSCOPE_ALGO_EXTENSION` to a local `.lbug_extension` file to override +the downloaded one (e.g. a build from +[LadybugDB/extensions](https://github.com/LadybugDB/extensions/tree/main/algo)). + +From source: + +```bash +bash scripts/download_icebug.sh # prebuilt libnetworkit for your platform -> ./icebug/ +bash scripts/download-liblbug.sh # prebuilt shared liblbug (dlopen needs shared, not static) -> ./liblbug/ +bash scripts/vendor_arrow.sh # stage libarrow/libomp next to libnetworkit (the algo + # extension references @rpath/libarrow, resolved via our rpaths) +cargo test --test gds_page_rank # GDS_PAGE_RANK vs the expected ranks (skips when offline) +``` + +The `icebug-analytics` cargo feature (on by default; `--no-default-features` to skip) links the +icebug Rust crate for in-process analytics such as `backend::graphr_page_rank`. + ## Project structure | Path | Contents | diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..2f65bde --- /dev/null +++ b/build.rs @@ -0,0 +1,91 @@ +use std::path::PathBuf; + +fn main() { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let target = std::env::var("TARGET").unwrap_or_default(); + let is_msvc = target.contains("msvc"); + + // The lbug docs require binaries (not just rlibs) to act like a library + // so that `LOAD EXTENSION` can resolve ladybug symbols at load time. + // `-rdynamic` is an ELF-linker flag: it is a no-op error on Windows/MSVC + // and actively harmful on Apple targets (clang parses it as `-r`, + // producing a relocatable object and dropping framework linkage). + // macOS extensions are built `-undefined dynamic_lookup`, so nothing is + // needed there. https://docs.ladybugdb.com/extensions/ + if !is_msvc && !target.contains("apple") { + println!("cargo:rustc-link-arg=-rdynamic"); + } + + // The lbug crate links the shared liblbug from LBUG_LIBRARY_DIR (see + // .cargo/config.toml); fail fast with a helpful message instead of a + // cryptic link error when the prebuilt hasn't been downloaded. + let lbug_dir = std::env::var_os("LBUG_LIBRARY_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(&manifest_dir).join("liblbug")); + let lbug_dir = if lbug_dir.is_absolute() { + lbug_dir + } else { + PathBuf::from(&manifest_dir).join(&lbug_dir) + }; + let lbug_lib = ["liblbug.dylib", "liblbug.so", "lbug_shared.dll", "lbug.lib"] + .iter() + .any(|n| lbug_dir.join(n).exists()); + if !lbug_lib { + panic!( + "\n\n\ + ============================================================\n\ + ERROR: Shared liblbug prebuilt not found at {}\n\n\ + Run `bash scripts/download-liblbug.sh` to download it from\n\ + GitHub releases.\n\ + ============================================================\n\n", + lbug_dir.display() + ); + } + + // The icebug Rust crate links libnetworkit from ICEBUG_DIR (see + // .cargo/config.toml); fail fast with a helpful message instead of a + // cryptic link error when the prebuilt hasn't been downloaded. + if std::env::var("CARGO_FEATURE_ICEBUG_ANALYTICS").is_ok() { + let icebug_dir = std::env::var_os("ICEBUG_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(&manifest_dir).join("icebug")); + // ICEBUG_DIR from .cargo/config.toml is relative to the config file + // (repo root == manifest dir here), so join when relative. + let icebug_dir = if icebug_dir.is_absolute() { + icebug_dir + } else { + PathBuf::from(&manifest_dir).join(&icebug_dir) + }; + let lib_dir = icebug_dir.join("lib"); + let has_lib = ["libnetworkit.dylib", "libnetworkit.so", "networkit.lib"] + .iter() + .any(|n| lib_dir.join(n).exists()); + if !has_lib || !icebug_dir.join("include").join("networkit").exists() { + panic!( + "\n\n\ + ============================================================\n\ + ERROR: Icebug prebuilt not found at {}\n\n\ + Run `bash scripts/download_icebug.sh` to download it from\n\ + GitHub releases (or build with `--no-default-features` to\n\ + skip icebug analytics).\n\ + ============================================================\n\n", + icebug_dir.display() + ); + } + + // Windows-specific (mirrors ../bugscope/src-tauri/build.rs): the + // icebug Windows release splits Networkit into lib/networkit.lib + // (static, with dllimport for GlobalState) and + // lib/networkit/networkit_state.lib (DLL import lib exporting + // GlobalState). The icebug crate's build.rs only links + // networkit.lib, missing GlobalState — add it here. + if target.contains("windows") { + let search = lib_dir.join("networkit"); + if search.join("networkit_state.lib").exists() { + println!("cargo:rustc-link-search=native={}", search.display()); + println!("cargo:rustc-link-lib=dylib=networkit_state"); + println!("cargo:warning=Linked networkit_state.lib for GlobalState symbols"); + } + } + } +} diff --git a/scripts/download-liblbug.sh b/scripts/download-liblbug.sh new file mode 100755 index 0000000..0ca3914 --- /dev/null +++ b/scripts/download-liblbug.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Download the prebuilt shared liblbug (LadybugDB core) into ./liblbug. +# +# The shared library (not the static archive) is required so that the +# dlopen'd algo extension can resolve ladybug symbols from the host process +# at LOAD EXTENSION time. With a static liblbug.a, dead-code stripping drops +# unreferenced symbols (e.g. GDSFunction::getLogicalPlan) and the extension +# fails to load. Mirrors ../bugscope/scripts/download-liblbug.sh. +# +# Version defaults to the lbug crate in Cargo.toml (must match Cargo.lock). +set -euo pipefail + +LIB_KIND="${LBUG_LIB_KIND:-shared}" +REPOSITORY="${LBUG_GITHUB_REPOSITORY:-LadybugDB/ladybug}" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TARGET_DIR="${LBUG_TARGET_DIR:-$PROJECT_DIR/liblbug}" + +if [ "$LIB_KIND" != "shared" ] && [ "$LIB_KIND" != "static" ]; then + echo "Unsupported LBUG_LIB_KIND: $LIB_KIND (expected 'shared' or 'static')" >&2 + exit 1 +fi + +# Single source of truth is the lbug dependency in Cargo.toml. +LBUG_VERSION="${LBUG_VERSION:-$(grep -E '^lbug *=.*version *=' "$PROJECT_DIR/Cargo.toml" | head -n1 | grep -oE '"[^"]+"' | head -n1 | tr -d '"')}" +if [ -z "$LBUG_VERSION" ]; then + echo "could not resolve lbug version from Cargo.toml" >&2 + exit 1 +fi + +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Darwin) + case "$ARCH" in + arm64|aarch64) MACOS_ARCH=arm64 ;; + x86_64) MACOS_ARCH=x86_64 ;; + *) echo "Unsupported macOS architecture: $ARCH" >&2; exit 1 ;; + esac + if [ "$LIB_KIND" = "static" ]; then + ARCHIVE="liblbug-static-osx-${MACOS_ARCH}.tar.gz" + LIB_NAME="liblbug.a" + else + ARCHIVE="liblbug-osx-${MACOS_ARCH}.tar.gz" + LIB_NAME="liblbug.dylib" + fi + ;; + Linux) + case "$ARCH" in + x86_64) LINUX_ARCH=x86_64 ;; + aarch64|arm64) LINUX_ARCH=aarch64 ;; + *) echo "Unsupported Linux architecture: $ARCH" >&2; exit 1 ;; + esac + if [ "$LIB_KIND" = "static" ]; then + ARCHIVE="liblbug-static-linux-${LINUX_ARCH}-compat.tar.gz" + LIB_NAME="liblbug.a" + else + ARCHIVE="liblbug-linux-${LINUX_ARCH}.tar.gz" + LIB_NAME="liblbug.so" + fi + ;; + MINGW*|MSYS*|CYGWIN*) + if [ "$LIB_KIND" = "static" ]; then + ARCHIVE="liblbug-static-windows-x86_64.zip" + LIB_NAME="lbug.lib" + else + ARCHIVE="liblbug-windows-x86_64.zip" + LIB_NAME="lbug_shared.dll" + fi + ;; + *) + echo "Unsupported OS: $OS" >&2 + exit 1 + ;; +esac + +if [ -f "$TARGET_DIR/$LIB_NAME" ]; then + echo "liblbug already exists in $TARGET_DIR" + exit 0 +fi + +mkdir -p "$TARGET_DIR" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +DOWNLOAD_URL="https://github.com/${REPOSITORY}/releases/download/v${LBUG_VERSION}/${ARCHIVE}" +echo "Downloading $DOWNLOAD_URL ..." +curl -fSL "$DOWNLOAD_URL" -o "$TMPDIR/$ARCHIVE" + +if [[ "$ARCHIVE" == *.zip ]]; then + unzip -o -q "$TMPDIR/$ARCHIVE" -d "$TARGET_DIR" +else + tar xzf "$TMPDIR/$ARCHIVE" -C "$TARGET_DIR" +fi + +if [ ! -f "$TARGET_DIR/$LIB_NAME" ]; then + echo "Expected liblbug library not found at $TARGET_DIR/$LIB_NAME" >&2 + echo "Archive contents:" >&2 + ls "$TARGET_DIR" >&2 + exit 1 +fi + +echo "Installed $ARCHIVE (lbug $LBUG_VERSION) to $TARGET_DIR" diff --git a/scripts/download_icebug.sh b/scripts/download_icebug.sh new file mode 100755 index 0000000..6434573 --- /dev/null +++ b/scripts/download_icebug.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Download the platform-specific Icebug prebuilt into ./icebug. +# +# Mirrors ../bugscope/scripts/download_icebug.sh: the icebug Rust crate's +# build script links against ICEBUG_DIR (set in .cargo/config.toml), and the +# algo Ladybug extension's CMake build consumes the same prebuilt +# (headers in include/, libnetworkit in lib/). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +REPOSITORY="${ICEBUG_GITHUB_REPOSITORY:-Ladybug-Memory/icebug}" +TARGET_DIR="${ICEBUG_TARGET_DIR:-$PROJECT_DIR/icebug}" +VERSION="${ICEBUG_VERSION:-13.2}" + +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Darwin) + ASSET_OS="macos" + ;; + Linux) + ASSET_OS="linux" + ;; + MINGW*|MSYS*|CYGWIN*) + ASSET_OS="win" + ;; + *) + echo "Unsupported OS: $OS" >&2 + exit 1 + ;; +esac + +case "$ARCH" in + arm64|aarch64) + ASSET_ARCH="arm64" + ;; + x86_64) + if [ "$ASSET_OS" = "win" ]; then + ASSET_ARCH="amd64" + else + ASSET_ARCH="x86_64" + fi + ;; + *) + echo "Unsupported architecture: $ARCH" >&2 + exit 1 + ;; +esac + +if [ "$ASSET_OS" = "win" ]; then + ARCHIVE="icebug-${ASSET_OS}-${ASSET_ARCH}.zip" +else + ARCHIVE="icebug-${ASSET_OS}-${ASSET_ARCH}.tar.gz" +fi + +LIB_NAME="libnetworkit.so" +case "$ASSET_OS" in + macos) + LIB_NAME="libnetworkit.dylib" + ;; + win) + LIB_NAME="networkit.lib" + ;; +esac + +if [ -f "$TARGET_DIR/lib/$LIB_NAME" ] && [ -d "$TARGET_DIR/include/networkit" ]; then + echo "icebug already exists in $TARGET_DIR" + exit 0 +fi + +mkdir -p "$TARGET_DIR" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +DOWNLOAD_URL="https://github.com/${REPOSITORY}/releases/download/${VERSION}/${ARCHIVE}" +echo "Downloading $DOWNLOAD_URL ..." +curl -fSL "$DOWNLOAD_URL" -o "$TMPDIR/$ARCHIVE" + +rm -rf "$TARGET_DIR/include" "$TARGET_DIR/lib" "$TARGET_DIR/extlibs" +if [[ "$ARCHIVE" == *.zip ]]; then + unzip -q "$TMPDIR/$ARCHIVE" -d "$TARGET_DIR" +else + tar xzf "$TMPDIR/$ARCHIVE" -C "$TARGET_DIR" +fi + +if [ ! -f "$TARGET_DIR/lib/$LIB_NAME" ]; then + echo "Expected Icebug library not found at $TARGET_DIR/lib/$LIB_NAME" >&2 + exit 1 +fi + +echo "Installed $ARCHIVE to $TARGET_DIR" diff --git a/scripts/stage_macos_bundle.sh b/scripts/stage_macos_bundle.sh new file mode 100755 index 0000000..87958fa --- /dev/null +++ b/scripts/stage_macos_bundle.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Stage native deps into a macOS .app bundle and rewrite load commands to +# @rpath. Adapted from ../bugscope/scripts/{stage_macos_frameworks,patch_macos_bundle_dylibs}.sh. +# +# bash scripts/stage_macos_bundle.sh dist/Bugscope.app +# +# Result: +# Contents/MacOS/bugscope (already placed by the caller) +# Contents/Frameworks/libnetworkit.dylib +# Contents/Frameworks/libarrow..dylib (soname form: the INSTALL'd +# algo extension references @rpath/libarrow., so keep the name) +# Contents/Frameworks/libomp.dylib +# Contents/Frameworks/liblbug.0*.dylib +set -euo pipefail + +APP_DIR="${1:?usage: stage_macos_bundle.sh }" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ICEBUG_DIR="${ICEBUG_DIR:-$PROJECT_DIR/icebug}" + +if [ "$(uname -s)" != "Darwin" ]; then + echo "stage_macos_bundle.sh is macOS-only" >&2 + exit 1 +fi + +MACOS_DIR="$APP_DIR/Contents/MacOS" +FRAMEWORKS_DIR="$APP_DIR/Contents/Frameworks" +BINARY="$MACOS_DIR/bugscope" +NETWORKIT_SRC="$ICEBUG_DIR/lib/libnetworkit.dylib" +LBUG_DIR="${LBUG_DIR:-$PROJECT_DIR/liblbug}" + +[ -f "$BINARY" ] || { echo "missing $BINARY" >&2; exit 1; } +[ -f "$NETWORKIT_SRC" ] || { echo "missing $NETWORKIT_SRC (run scripts/download_icebug.sh)" >&2; exit 1; } +LBUG_SRC="$(ls "$LBUG_DIR"/liblbug.0*.dylib 2>/dev/null | head -n1)" +[ -n "$LBUG_SRC" ] || { echo "missing liblbug.0*.dylib in $LBUG_DIR (run scripts/download-liblbug.sh)" >&2; exit 1; } + +dep_path() { + otool -L "$1" | awk -v pattern="$2" '$1 ~ pattern { print $1; exit }' +} + +mkdir -p "$FRAMEWORKS_DIR" +cp -f "$NETWORKIT_SRC" "$FRAMEWORKS_DIR/libnetworkit.dylib" +cp -f "$LBUG_SRC" "$FRAMEWORKS_DIR/$(basename "$LBUG_SRC")" +[ -f "$LBUG_DIR/liblbug.dylib" ] && cp -f "$LBUG_DIR/liblbug.dylib" "$FRAMEWORKS_DIR/liblbug.dylib" + +# Keep the SONAME filename (libarrow..dylib): the INSTALL'd algo extension +# references @rpath/libarrow., so renaming would break its dlopen. +# Prefer the vendored copy (scripts/vendor_arrow.sh) so bundled arrow matches +# what the extension was built against; fall back to the brew original. +ARROW_SRC="$(dep_path "$NETWORKIT_SRC" 'libarrow[.].*[.]dylib')" +ARROW_NAME="$(basename "$ARROW_SRC")" +if [ -f "$ICEBUG_DIR/lib/$ARROW_NAME" ]; then + cp -f "$ICEBUG_DIR/lib/$ARROW_NAME" "$FRAMEWORKS_DIR/$ARROW_NAME" +else + case "$ARROW_SRC" in + /opt/homebrew/*|/usr/local/*) cp -fL "$ARROW_SRC" "$FRAMEWORKS_DIR/$ARROW_NAME" ;; + *) echo "cannot stage arrow dep: $ARROW_SRC" >&2; exit 1 ;; + esac +fi + +OMP_SRC="$(dep_path "$NETWORKIT_SRC" 'libomp[.]dylib')" +case "$OMP_SRC" in + /opt/homebrew/*|/usr/local/*) cp -f "$OMP_SRC" "$FRAMEWORKS_DIR/libomp.dylib" ;; + "") echo "no libomp dep in libnetworkit; skipping" ;; + *) echo "cannot stage omp dep: $OMP_SRC" >&2; exit 1 ;; +esac + +change_if_present() { + if otool -L "$1" | awk '{ print $1 }' | grep -Fxq "$2"; then + install_name_tool -change "$2" "$3" "$1" + fi +} + +patch_binary() { + local binary="$1" + while IFS= read -r dep; do + case "$dep" in + *libarrow*.dylib) change_if_present "$binary" "$dep" "@rpath/$ARROW_NAME" ;; + *libomp.dylib) change_if_present "$binary" "$dep" "@rpath/libomp.dylib" ;; + *libnetworkit.dylib) change_if_present "$binary" "$dep" "@rpath/libnetworkit.dylib" ;; + *liblbug*.dylib) change_if_present "$binary" "$dep" "@rpath/$(basename "$LBUG_SRC")" ;; + esac + done < <(otool -L "$binary" | awk 'NR > 1 { print $1 }') +} + +chmod u+w "$BINARY" "$FRAMEWORKS_DIR"/*.dylib +patch_binary "$BINARY" +for dylib in "$FRAMEWORKS_DIR"/*.dylib; do + install_name_tool -id "@rpath/$(basename "$dylib")" "$dylib" + patch_binary "$dylib" +done +chmod -w "$FRAMEWORKS_DIR"/*.dylib 2>/dev/null || true + +echo "Staged macOS bundle in $APP_DIR" diff --git a/scripts/vendor_arrow.sh b/scripts/vendor_arrow.sh new file mode 100755 index 0000000..cf18fa3 --- /dev/null +++ b/scripts/vendor_arrow.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Vendor libarrow + libomp shared libraries into ./icebug/lib so the +# DOWNLOADED algo extension (INSTALL algo) can dlopen: it references +# `@rpath/libarrow.` / `libarrow.so.`, resolved against the +# host process rpaths (see .cargo/config.toml) — not against absolute +# system paths. Same mechanism scripts/stage_macos_bundle.sh uses for dist. +# +# macOS: copies from Homebrew (apache-arrow, libomp). +# Linux: copies from ldconfig / well-known apt locations. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +LIB_DIR="${ICEBUG_LIB_DIR:-$PROJECT_DIR/icebug/lib}" +mkdir -p "$LIB_DIR" + +search_dirs() { + if [ "$(uname -s)" = "Darwin" ]; then + echo "/opt/homebrew/opt/apache-arrow/lib /usr/local/opt/apache-arrow/lib" + echo "/opt/homebrew/opt/libomp/lib /usr/local/opt/libomp/lib" + else + echo "/usr/lib/x86_64-linux-gnu /usr/lib/aarch64-linux-gnu /usr/lib64 /usr/lib /usr/local/lib" + fi +} + +vendored=0 +for d in $(search_dirs); do + [ -d "$d" ] || continue + for src in "$d"/libarrow.so* "$d"/libarrow.*.dylib "$d"/libomp.so* "$d"/libomp.dylib; do + [ -e "$src" ] || continue + case "$src" in + *.so) ;; # dev symlink, skip (runtime needs the soname file) + *) : ;; + esac + base="$(basename "$src")" + # Skip bare dev symlinks like libarrow.so / libarrow.dylib. + case "$base" in + libarrow.so|libarrow.dylib|libomp.so) continue ;; + esac + if [ ! -f "$LIB_DIR/$base" ]; then + cp -fL "$src" "$LIB_DIR/$base" + echo "vendored $src -> $LIB_DIR/$base" + vendored=1 + fi + done +done + +if [ "$vendored" -eq 0 ] && ! ls "$LIB_DIR" | grep -qE "libarrow|libomp"; then + echo "warning: no libarrow/libomp found to vendor; INSTALL'd extension may fail to LOAD" >&2 +fi diff --git a/src/backend.rs b/src/backend.rs index 776cf9d..49d9f4c 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -158,7 +158,128 @@ pub fn open_connection(path: &str) -> Result> { // Tauri backend which re-opens per command and drops at command end. Here a // short-lived query holds both alive via the leaked handle. let db: &'static Database = Box::leak(Box::new(db)); - Connection::new(db).context("failed to create connection") + let conn = Connection::new(db).context("failed to create connection")?; + // Best-effort: make the icebug-backed algo extension (PAGE_RANK, + // GDS_PAGE_RANK, PROJECT_GRAPH, …) available to Cypher. Missing bundle + // just means those functions are unavailable; everything else works. + ensure_algo_extension(&conn); + Ok(conn) +} + +/// Install the algo extension from the official repo +/// (`https://extension.ladybugdb.com`). A no-op when already installed. +/// Returns false when the download fails (e.g. offline). Never fails the caller. +pub fn install_algo_extension(conn: &Connection) -> bool { + conn.query("INSTALL algo").is_ok() +} + +/// Load the algo extension on this connection. Returns true when the +/// GDS_* surface is available. Never fails the caller. +/// +/// Must run per connection, not once per process: ladybug tracks loaded +/// extensions per Database (`ExtensionManager` lives on the `Database`), and +/// this app opens a fresh `Database` per `open_connection` call. A cached +/// "already loaded" result would leave other connections without GDS. +/// +/// Resolution order: `LOAD algo` (already installed), otherwise +/// `INSTALL algo` from the official repo followed by `LOAD algo`. As an +/// escape hatch for local extension builds, `BUGSCOPE_ALGO_EXTENSION` may +/// point at a `.lbug_extension` file to `LOAD EXTENSION` directly. +pub fn ensure_algo_extension(conn: &Connection) -> bool { + if let Some(path) = std::env::var_os("BUGSCOPE_ALGO_EXTENSION").map(PathBuf::from) { + // LOAD EXTENSION takes a path without the suffix on some builds; + // try the full path first, then the stem form. + let full = path.to_string_lossy().replace('\\', "/"); + if conn.query(&format!("LOAD EXTENSION '{full}'")).is_ok() { + return true; + } + let stem = full + .strip_suffix(".lbug_extension") + .unwrap_or(&full) + .to_string(); + if conn.query(&format!("LOAD EXTENSION '{stem}'")).is_ok() { + return true; + } + } + if conn.query("LOAD algo").is_ok() { + return true; + } + // Not installed yet: fetch it, then load. + if install_algo_extension(conn) { + return conn.query("LOAD algo").is_ok(); + } + false +} + +/// In-process PageRank over an already-collected graph via the linked icebug +/// (NetworKit) library — no extension needed. Returns `(node_id, score)` +/// sorted by score descending. Mirrors the GDS_PAGE_RANK ranking so the +/// extension result can be cross-checked (see `tests/gds_page_rank.rs`). +#[cfg(feature = "icebug-analytics")] +pub fn graphr_page_rank(data: &GraphData) -> Vec<(String, f64)> { + use arrow::array::UInt64Array; + let index: HashMap<&str, u64> = data + .nodes + .iter() + .enumerate() + .map(|(i, n)| (n.id.as_str(), i as u64)) + .collect(); + let n = data.nodes.len() as u64; + if n == 0 { + return Vec::new(); + } + // Directed GraphR needs both out- and in-edge CSRs. + let mut outgoing: Vec> = vec![Vec::new(); n as usize]; + let mut incoming: Vec> = vec![Vec::new(); n as usize]; + for l in &data.links { + if let (Some(&s), Some(&t)) = (index.get(l.source.as_str()), index.get(l.target.as_str())) { + if s != t { + outgoing[s as usize].push(t); + incoming[t as usize].push(s); + } + } + } + fn pack(adj: Vec>) -> (Vec, Vec) { + let mut indptr = Vec::with_capacity(adj.len() + 1); + let mut indices = Vec::new(); + for mut neighbors in adj { + neighbors.sort_unstable(); + neighbors.dedup(); + indptr.push(indices.len() as u64); + indices.extend(neighbors); + } + indptr.push(indices.len() as u64); + (indices, indptr) + } + let (out_indices, out_indptr) = pack(outgoing); + let (in_indices, in_indptr) = pack(incoming); + let Ok(graph) = icebug::GraphR::from_directed_csr( + n, + UInt64Array::from(out_indices), + UInt64Array::from(out_indptr), + UInt64Array::from(in_indices), + UInt64Array::from(in_indptr), + ) else { + return Vec::new(); + }; + // normalized=false: scores form a sum-to-1 distribution like GDS_PAGE_RANK. + let Ok(mut pr) = icebug::PageRank::new(&graph, 0.85, 1e-9, false) else { + return Vec::new(); + }; + if pr.run().is_err() { + return Vec::new(); + } + let Ok(scores) = pr.scores() else { + return Vec::new(); + }; + let mut out: Vec<(String, f64)> = data + .nodes + .iter() + .enumerate() + .map(|(i, nd)| (nd.id.clone(), *scores.get(i).unwrap_or(&0.0))) + .collect(); + out.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + out } /// (table_id, name, kind) for every table — `CALL SHOW_TABLES` is tiny. @@ -212,7 +333,6 @@ fn collect_edges_arrow( links: &mut Vec, node_ids: &mut HashSet, ) -> Result<()> { - use arrow::array::Array; let mut result = conn .query_as_arrow( &format!( diff --git a/tests/gds_page_rank.rs b/tests/gds_page_rank.rs new file mode 100644 index 0000000..be78902 --- /dev/null +++ b/tests/gds_page_rank.rs @@ -0,0 +1,142 @@ +//! GDS_PAGE_RANK coverage, ported from +//! https://github.com/LadybugDB/extensions/blob/main/algo/test/test_files/gds_page_rank.test +//! +//! Requires the bundled algo extension (`extensions/algo/libalgo.lbug_extension`, +//! built by `scripts/build_algo_extension.sh`, or `BUGSCOPE_ALGO_EXTENSION`). +//! The test skips gracefully when the extension is absent so `cargo test` +//! stays green on machines without the native bundle. + +use bugscope::backend::{self, GraphData}; +use lbug::{Database, SystemConfig, Value}; + +fn open_ephemeral() -> (tempfile::TempDir, Database) { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("gds.lbdb"); + let db = Database::new(path.to_str().unwrap(), SystemConfig::default()).unwrap(); + (dir, db) +} + +fn exec(conn: &lbug::Connection, q: &str) { + conn.query(q) + .unwrap_or_else(|e| panic!("query failed: {q}\n{e}")); +} + +/// (id, rank) rows from `CALL GDS_PAGE_RANK(...) RETURN node.id, rank ...`. +fn page_rank_rows(conn: &lbug::Connection, graph: &str) -> Vec<(i64, f64)> { + let mut out = Vec::new(); + let mut result = conn + .query(&format!( + "CALL GDS_PAGE_RANK('{graph}') RETURN node.id, rank ORDER BY rank DESC, node.id" + )) + .expect("GDS_PAGE_RANK call failed"); + for row in &mut result { + let (Value::Int64(id), Value::Double(rank)) = (&row[0], &row[1]) else { + panic!("unexpected GDS_PAGE_RANK row: {row:?}"); + }; + out.push((*id, *rank)); + } + out +} + +fn approx(a: f64, b: f64) -> bool { + (a - b).abs() < 1e-5 +} + +#[test] +fn gds_page_rank_star() { + let (_dir, db) = open_ephemeral(); + let conn = lbug::Connection::new(&db).unwrap(); + // INSTALL first: skip only when the extension repo is unreachable + // (offline). A successful install followed by a failed LOAD is a real + // failure, so assert instead of skipping. + if !backend::install_algo_extension(&conn) { + eprintln!("SKIP gds_page_rank_star: cannot INSTALL algo (offline?)"); + return; + } + assert!( + backend::ensure_algo_extension(&conn), + "algo installed but LOAD algo failed" + ); + + // --- GDSPageRankStar setup --- + exec(&conn, "CREATE NODE TABLE N(id INT64 PRIMARY KEY)"); + exec(&conn, "CREATE REL TABLE E(FROM N TO N)"); + exec( + &conn, + "CREATE (a:N{id:0}), (b:N{id:1}), (c:N{id:2}), (d:N{id:3})", + ); + for leaf in [1, 2, 3] { + exec( + &conn, + &format!("MATCH (x:N{{id:{leaf}}}), (y:N{{id:0}}) CREATE (x)-[:E]->(y)"), + ); + } + + // Plain projection consumes the materialized arrow CSR (zero-copy path). + exec(&conn, "CALL PROJECT_GRAPH('G', ['N'], ['E'])"); + let rows = page_rank_rows(&conn, "G"); + assert_eq!(rows.len(), 4); + // Hub (id 0) ranks highest; scores are a sum-to-1 distribution. + assert_eq!(rows[0].0, 0); + assert!(approx(rows[0].1, 0.479_730)); + for (_, rank) in rows.iter().skip(1) { + assert!(approx(*rank, 0.173_423), "leaf rank {rank}"); + } + let total: f64 = rows.iter().map(|(_, r)| r).sum(); + assert!(approx(total, 1.0), "sum-to-1 distribution, got {total}"); + + // Filtered projection goes through the scan fallback: identical ranks. + exec( + &conn, + "CALL PROJECT_GRAPH('GPred', ['N'], {E: 'r.rowid >= 0'})", + ); + assert_eq!(page_rank_rows(&conn, "GPred"), rows); + + // Cross-check: the in-process icebug PageRank agrees on the ORDERING — + // hub first, leaves tied. (Exact scores differ: sink/damping conventions + // vary between implementations; the GDS values above are authoritative.) + let data: GraphData = backend::run_cypher(&conn, "MATCH (a)-[r]->(b) RETURN a, r, b").unwrap(); + let local = backend::graphr_page_rank(&data); + assert_eq!(local.len(), 4); + assert_eq!(local[0].0, "0:0"); + assert!( + local.iter().skip(1).all(|(_, r)| approx(*r, local[1].1)), + "leaves tied: {local:?}" + ); +} + +#[test] +fn gds_page_rank_stale_projection_serves_live_storage() { + let (_dir, db) = open_ephemeral(); + let conn = lbug::Connection::new(&db).unwrap(); + if !backend::install_algo_extension(&conn) { + eprintln!("SKIP gds_page_rank_stale_projection: cannot INSTALL algo (offline?)"); + return; + } + assert!( + backend::ensure_algo_extension(&conn), + "algo installed but LOAD algo failed" + ); + + exec(&conn, "CREATE NODE TABLE N(id INT64 PRIMARY KEY)"); + exec(&conn, "CREATE REL TABLE E(FROM N TO N)"); + exec( + &conn, + "CREATE (a:N{id:0}), (b:N{id:1}), (c:N{id:2}), (d:N{id:3})", + ); + for leaf in [1, 2] { + exec( + &conn, + &format!("MATCH (x:N{{id:{leaf}}}), (y:N{{id:0}}) CREATE (x)-[:E]->(y)"), + ); + } + exec(&conn, "CALL PROJECT_GRAPH('GS', ['N'], ['E'])"); + // Mutate AFTER projection (node cardinality unchanged): the pinned CSR is + // stale, the epoch check must reject it, and the fallback serves LIVE + // storage — the full 3-leaf star, not the 2-edge projected graph. + exec(&conn, "MATCH (x:N{id:3}), (y:N{id:0}) CREATE (x)-[:E]->(y)"); + let rows = page_rank_rows(&conn, "GS"); + assert_eq!(rows.len(), 4); + assert_eq!(rows[0].0, 0); + assert!(approx(rows[0].1, 0.479_730)); +} From c84e716c6359e159c3846deae43a9156eaf16721 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Thu, 1 Oct 2026 19:46:24 -0700 Subject: [PATCH 2/3] CI: package icebug/algo extension and run GDS test on all platforms - Download icebug prebuilt + shared liblbug in CI and release jobs - Vendor arrow/openmp runtimes next to libnetworkit (Unix) so the INSTALL'd algo extension resolves @rpath/libarrow - Add Test GDS_PAGE_RANK step (cargo test --lib --test gds_page_rank) with LD_LIBRARY_PATH on Unix and icebug/lib + liblbug + vcpkg bin on PATH for Windows - Bundle icebug shared libs + algo extension into every release archive (macOS .app via stage_macos_bundle.sh, Linux tarball with best-effort libomp/libarrow vendoring, Windows zip with DLLs + vcpkg arrow DLLs) - Linux/macOS deps: LLVM libomp + Apache Arrow repos, cmake, libarrow-dev/libomp-20-dev, brew apache-arrow - Windows: vcpkg manifest (arrow, pkgconf), lukka/run-vcpkg setup with pinned commit, NuGet GitHub Packages binary caching, PKG_CONFIG_PATH/CMAKE_PREFIX_PATH/CXXFLAGS env for the icebug crate's pkg-config probe --- .github/workflows/build.yml | 133 +++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5bb8b87..39e7110 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,7 @@ on: permissions: contents: read + packages: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -30,8 +31,14 @@ jobs: run: | # lbug's C++ bridge needs (C++20): jammy's gcc-11 is too old. sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + # LLVM repo for libomp matching the icebug prebuilt binaries. + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/llvm-snapshot.asc + echo "deb http://apt.llvm.org/jammy llvm-toolchain-jammy-20 main" | sudo tee /etc/apt/sources.list.d/llvm20.list + # Apache Arrow repo (icebug's cxx bridge needs arrow headers). + wget https://apache.jfrog.io/artifactory/arrow/ubuntu/apache-arrow-apt-source-latest-jammy.deb + sudo apt-get install -y ./apache-arrow-apt-source-latest-jammy.deb sudo apt-get update - sudo apt-get install -y --no-install-recommends gcc-13 g++-13 pkg-config libssl-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev + sudo apt-get install -y --no-install-recommends gcc-13 g++-13 cmake pkg-config libssl-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libarrow-dev libomp-20-dev echo "CC=gcc-13" >> "$GITHUB_ENV" echo "CXX=g++-13" >> "$GITHUB_ENV" @@ -66,6 +73,12 @@ jobs: key: lbug-prebuilt-${{ env.LBUG_VERSION }}-${{ runner.os }} restore-keys: lbug-prebuilt- + - name: Download icebug prebuilt + run: bash scripts/download_icebug.sh + + - name: Download shared liblbug + run: bash scripts/download-liblbug.sh + - name: Rust cache uses: Swatinem/rust-cache@v2 with: @@ -118,15 +131,21 @@ jobs: - name: Install dependencies (macOS) if: matrix.platform == 'macos-latest' - run: brew install libomp openssl + run: brew install libomp apache-arrow cmake openssl - name: Install dependencies (Linux) if: matrix.platform == 'ubuntu-22.04' run: | # lbug's C++ bridge needs (C++20): jammy's gcc-11 is too old. sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + # LLVM repo for libomp matching the icebug prebuilt binaries. + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/llvm-snapshot.asc + echo "deb http://apt.llvm.org/jammy llvm-toolchain-jammy-20 main" | sudo tee /etc/apt/sources.list.d/llvm20.list + # Apache Arrow repo (icebug's cxx bridge needs arrow headers). + wget https://apache.jfrog.io/artifactory/arrow/ubuntu/apache-arrow-apt-source-latest-jammy.deb + sudo apt-get install -y ./apache-arrow-apt-source-latest-jammy.deb sudo apt-get update - sudo apt-get install -y --no-install-recommends gcc-13 g++-13 pkg-config libssl-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev + sudo apt-get install -y --no-install-recommends gcc-13 g++-13 cmake pkg-config libssl-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libarrow-dev libomp-20-dev echo "CC=gcc-13" >> "$GITHUB_ENV" echo "CXX=g++-13" >> "$GITHUB_ENV" @@ -154,6 +173,44 @@ jobs: Get-ChildItem $stage | Select-Object Name, Length echo "LIB=$stage" >> "$env:GITHUB_ENV" + - name: Create vcpkg manifest (Windows) + if: matrix.platform == 'windows-2022' + shell: bash + run: | + cat > vcpkg.json << 'EOF' + { + "name": "bugscope-deps", + "version": "1.0.0", + "dependencies": [ + "arrow", + "pkgconf" + ] + } + EOF + + - name: Setup vcpkg (Windows) + if: matrix.platform == 'windows-2022' + uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: '14d83b1c34655a6af6f044aa1679f668c0b0b70e' + + - name: Authenticate vcpkg NuGet (Windows) + if: matrix.platform == 'windows-2022' + shell: pwsh + run: | + $nuget = & $env:VCPKG_ROOT/vcpkg fetch nuget | Out-String + $nuget = $nuget.Trim() + & $nuget sources add -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" -Name "GitHubPackages" -Username "${{ github.repository_owner }}" -Password "${{ secrets.GITHUB_TOKEN }}" -StorePasswordInClearText -Verbosity detailed + & $nuget setapikey "${{ secrets.GITHUB_TOKEN }}" -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" + + - name: Install vcpkg packages (Windows) + if: matrix.platform == 'windows-2022' + shell: pwsh + run: | + & $env:VCPKG_ROOT/vcpkg install --recurse --clean-after-build --triplet x64-windows + env: + VCPKG_BINARY_SOURCES: 'clear;nuget,https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json,readwrite' + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: @@ -172,6 +229,55 @@ jobs: - name: Rust cache uses: Swatinem/rust-cache@v2 + - name: Download icebug prebuilt + shell: bash + run: bash scripts/download_icebug.sh + + - name: Download shared liblbug + shell: bash + run: bash scripts/download-liblbug.sh + + - name: Vendor arrow/openmp runtimes (macOS/Linux) + if: matrix.platform != 'windows-2022' + shell: bash + # The INSTALL'd algo extension references @rpath/libarrow, resolved + # against our rpaths: vendor the system libs next to libnetworkit. + run: bash scripts/vendor_arrow.sh + + - name: Configure macOS build env + if: matrix.platform == 'macos-latest' + shell: bash + run: | + # Apple clang doesn't search Homebrew libomp without this (icebug's + # cxx bridge includes networkit headers needing omp.h). Mirrors + # ../bugscope's CXXFLAGS on its macOS build step. + echo "CXXFLAGS=-I/opt/homebrew/opt/libomp/include" >> "$GITHUB_ENV" + + - name: Configure Windows build env + if: matrix.platform == 'windows-2022' + shell: bash + run: | + echo "PKG_CONFIG_PATH=$GITHUB_WORKSPACE/vcpkg_installed/x64-windows/lib/pkgconfig" >> "$GITHUB_ENV" + echo "CMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/vcpkg_installed/x64-windows" >> "$GITHUB_ENV" + echo "CXXFLAGS=/std:c++20" >> "$GITHUB_ENV" + + - name: Test GDS_PAGE_RANK + shell: bash + run: | + # The test INSTALLs the algo extension from the official repo and + # LOADs it at runtime: its libarrow dep resolves via our rpaths + # (vendored above) on Unix, via PATH (vcpkg) on Windows. + if [ "$RUNNER_OS" = "Windows" ]; then + export PATH="$(cygpath -w "$GITHUB_WORKSPACE/icebug/lib");$(cygpath -w "$GITHUB_WORKSPACE/liblbug");$(cygpath -w "$GITHUB_WORKSPACE/vcpkg_installed/x64-windows/bin");$PATH" + export PKG_CONFIG_PATH="$GITHUB_WORKSPACE/vcpkg_installed/x64-windows/lib/pkgconfig" + export CXXFLAGS="/std:c++20" + else + export LD_LIBRARY_PATH="$GITHUB_WORKSPACE/icebug/lib:$GITHUB_WORKSPACE/liblbug${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + fi + # --lib --test selects only these targets so the GUI binary + # itself is not linked here (it gets built in the next step). + cargo test --release --target ${{ matrix.target }} --lib --test gds_page_rank + - name: Build release binary run: cargo build --release --target ${{ matrix.target }} @@ -191,6 +297,8 @@ jobs: mkdir -p dist/Bugscope.app/Contents/MacOS dist/Bugscope.app/Contents/Resources cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" dist/Bugscope.app/Contents/MacOS/bugscope sed "s|@VERSION@|${version}|g" packaging/macos/Info.plist > dist/Bugscope.app/Contents/Info.plist + # Bundle icebug shared libs + algo extension (GDS_* via Cypher). + bash scripts/stage_macos_bundle.sh dist/Bugscope.app codesign --deep --force -s - dist/Bugscope.app archive="dist/bugscope-${version}-${{ matrix.target }}-macos.zip" ditto -c -k --keepParent dist/Bugscope.app "$archive" @@ -209,6 +317,17 @@ jobs: rm -rf dist && mkdir -p "dist/$name" cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "dist/$name/" strip "dist/$name/${{ matrix.binary }}" + # Bundle icebug shared libs + algo extension (GDS_* via Cypher). + # $ORIGIN/icebug/lib and $ORIGIN are baked rpaths (see .cargo/config.toml). + mkdir -p "dist/$name/icebug" "dist/$name/liblbug" + cp -r icebug/lib "dist/$name/icebug/" + cp liblbug/liblbug.so* "dist/$name/liblbug/" + # Best-effort: vendor libomp/libarrow so the tarball runs without + # system installs (the exe uses RPATH, not RUNPATH, so transitive + # deps resolve from these dirs too). + for dep in $(ldd "dist/$name/${{ matrix.binary }}" | grep -oE '/[^ ]*lib(arrow|omp)[^ ]*\.so[^ ]*' | sort -u); do + cp -f -n "$dep" "dist/$name/icebug/lib/" 2>/dev/null || true + done cp README.md "dist/$name/" echo "$version" > "dist/$name/VERSION" tar -C dist -czf "dist/$name.tar.gz" "$name" @@ -227,6 +346,14 @@ jobs: if (Test-Path dist) { Remove-Item -Recurse -Force dist } New-Item -ItemType Directory -Force "dist/$name" | Out-Null Copy-Item "target/${{ matrix.target }}/release/${{ matrix.binary }}", README.md "dist/$name/" + # Bundle icebug DLLs + algo extension (GDS_* via Cypher) next to the exe. + New-Item -ItemType Directory -Force "dist/$name/icebug/lib" | Out-Null + New-Item -ItemType Directory -Force "dist/$name/liblbug" | Out-Null + Get-ChildItem "icebug/lib" -Filter *.dll -Recurse | ForEach-Object { Copy-Item $_.FullName "dist/$name/icebug/lib/" } + Get-ChildItem "liblbug" -Filter *.dll | ForEach-Object { Copy-Item $_.FullName "dist/$name/liblbug/" } + # The INSTALL'd algo extension loads arrow dynamically: ship the + # vcpkg arrow DLLs next to the binary (on PATH at runtime). + Get-ChildItem "vcpkg_installed/x64-windows/bin" -Filter arrow*.dll | ForEach-Object { Copy-Item $_.FullName "dist/$name/icebug/lib/" } Compress-Archive -Path "dist/$name" -DestinationPath "dist/$name.zip" $hash = (Get-FileHash "dist/$name.zip" -Algorithm SHA256).Hash.ToLower() [IO.File]::WriteAllText("dist/$name.zip.sha256", "$hash $name.zip`n") From 3619bfa26a0936510ec8db8ebfee479755abb07e Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Thu, 1 Oct 2026 19:46:28 -0700 Subject: [PATCH 3/3] CI: harden dependency installs (timeouts, vcpkg cache, PATH) - Bound apt mirror stalls on Linux: wget --timeout/--tries, Acquire timeouts + retries, timeout-minutes on dep steps - Authenticate vcpkg NuGet with classic PAT (VCPKG_NUGET_PAT) instead of GITHUB_TOKEN: repo-scoped token 403s pushing new packages to the org feed so the binary cache never populated - Put vcpkg pkgconf on PATH for the icebug crate's pkg-config probe (convert the whole PATH with cygpath -p so later pwsh steps keep a resolvable Windows-form PATH) - Add icebug/lib/networkit to PATH for the Windows GDS test (networkit_state.dll lives in that subdir on the win prebuilt) --- .github/workflows/build.yml | 40 +++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39e7110..19dccfb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,17 +28,19 @@ jobs: - uses: actions/checkout@v4 - name: Install system dependencies (Linux) + timeout-minutes: 25 run: | # lbug's C++ bridge needs (C++20): jammy's gcc-11 is too old. sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test # LLVM repo for libomp matching the icebug prebuilt binaries. - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/llvm-snapshot.asc + wget -qO- --timeout=60 --tries=3 https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/llvm-snapshot.asc echo "deb http://apt.llvm.org/jammy llvm-toolchain-jammy-20 main" | sudo tee /etc/apt/sources.list.d/llvm20.list # Apache Arrow repo (icebug's cxx bridge needs arrow headers). - wget https://apache.jfrog.io/artifactory/arrow/ubuntu/apache-arrow-apt-source-latest-jammy.deb + wget --timeout=60 --tries=3 https://apache.jfrog.io/artifactory/arrow/ubuntu/apache-arrow-apt-source-latest-jammy.deb sudo apt-get install -y ./apache-arrow-apt-source-latest-jammy.deb - sudo apt-get update - sudo apt-get install -y --no-install-recommends gcc-13 g++-13 cmake pkg-config libssl-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libarrow-dev libomp-20-dev + # Bound mirror stalls: fail fast instead of hanging the run for hours. + sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=60 -o Acquire::https::Timeout=60 update + sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=60 -o Acquire::https::Timeout=60 install -y --no-install-recommends gcc-13 g++-13 cmake pkg-config libssl-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libarrow-dev libomp-20-dev echo "CC=gcc-13" >> "$GITHUB_ENV" echo "CXX=g++-13" >> "$GITHUB_ENV" @@ -134,18 +136,20 @@ jobs: run: brew install libomp apache-arrow cmake openssl - name: Install dependencies (Linux) + timeout-minutes: 25 if: matrix.platform == 'ubuntu-22.04' run: | # lbug's C++ bridge needs (C++20): jammy's gcc-11 is too old. sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test # LLVM repo for libomp matching the icebug prebuilt binaries. - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/llvm-snapshot.asc + wget -qO- --timeout=60 --tries=3 https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/llvm-snapshot.asc echo "deb http://apt.llvm.org/jammy llvm-toolchain-jammy-20 main" | sudo tee /etc/apt/sources.list.d/llvm20.list # Apache Arrow repo (icebug's cxx bridge needs arrow headers). - wget https://apache.jfrog.io/artifactory/arrow/ubuntu/apache-arrow-apt-source-latest-jammy.deb + wget --timeout=60 --tries=3 https://apache.jfrog.io/artifactory/arrow/ubuntu/apache-arrow-apt-source-latest-jammy.deb sudo apt-get install -y ./apache-arrow-apt-source-latest-jammy.deb - sudo apt-get update - sudo apt-get install -y --no-install-recommends gcc-13 g++-13 cmake pkg-config libssl-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libarrow-dev libomp-20-dev + # Bound mirror stalls: fail fast instead of hanging the run for hours. + sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=60 -o Acquire::https::Timeout=60 update + sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=60 -o Acquire::https::Timeout=60 install -y --no-install-recommends gcc-13 g++-13 cmake pkg-config libssl-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libarrow-dev libomp-20-dev echo "CC=gcc-13" >> "$GITHUB_ENV" echo "CXX=g++-13" >> "$GITHUB_ENV" @@ -198,10 +202,15 @@ jobs: if: matrix.platform == 'windows-2022' shell: pwsh run: | + # NOTE: must be a classic PAT with write:packages (repo secret + # VCPKG_NUGET_PAT), SSO-authorized if the org enforces SAML. + # GITHUB_TOKEN 403s on push: repo-scoped tokens cannot publish + # new, unlinked packages to the org-level feed, so the cache + # could never populate and every build rebuilt from source. $nuget = & $env:VCPKG_ROOT/vcpkg fetch nuget | Out-String $nuget = $nuget.Trim() - & $nuget sources add -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" -Name "GitHubPackages" -Username "${{ github.repository_owner }}" -Password "${{ secrets.GITHUB_TOKEN }}" -StorePasswordInClearText -Verbosity detailed - & $nuget setapikey "${{ secrets.GITHUB_TOKEN }}" -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" + & $nuget sources add -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" -Name "GitHubPackages" -Username "${{ github.repository_owner }}" -Password "${{ secrets.VCPKG_NUGET_PAT }}" -StorePasswordInClearText -Verbosity detailed + & $nuget setapikey "${{ secrets.VCPKG_NUGET_PAT }}" -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" - name: Install vcpkg packages (Windows) if: matrix.platform == 'windows-2022' @@ -260,6 +269,12 @@ jobs: echo "PKG_CONFIG_PATH=$GITHUB_WORKSPACE/vcpkg_installed/x64-windows/lib/pkgconfig" >> "$GITHUB_ENV" echo "CMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/vcpkg_installed/x64-windows" >> "$GITHUB_ENV" echo "CXXFLAGS=/std:c++20" >> "$GITHUB_ENV" + # pkgconf itself must be on PATH: the icebug crate probes arrow via + # pkg-config (mirrors ../bugscope's PATH setup on its build step). + # Convert the whole list with cygpath -p: appending a Windows entry + # to bash's Unix-form PATH would leave later pwsh steps with an + # unresolvable PATH (cargo not found). + echo "PATH=$(cygpath -w "$GITHUB_WORKSPACE/vcpkg_installed/x64-windows/tools/pkgconf");$(cygpath -wp "$PATH")" >> "$GITHUB_ENV" - name: Test GDS_PAGE_RANK shell: bash @@ -268,7 +283,10 @@ jobs: # LOADs it at runtime: its libarrow dep resolves via our rpaths # (vendored above) on Unix, via PATH (vcpkg) on Windows. if [ "$RUNNER_OS" = "Windows" ]; then - export PATH="$(cygpath -w "$GITHUB_WORKSPACE/icebug/lib");$(cygpath -w "$GITHUB_WORKSPACE/liblbug");$(cygpath -w "$GITHUB_WORKSPACE/vcpkg_installed/x64-windows/bin");$PATH" + # NB: networkit_state.dll lives in the icebug/lib/networkit + # subdir (the win prebuilt splits GlobalState out of the static + # networkit.lib); the loader needs that dir on PATH too. + export PATH="$(cygpath -w "$GITHUB_WORKSPACE/icebug/lib");$(cygpath -w "$GITHUB_WORKSPACE/icebug/lib/networkit");$(cygpath -w "$GITHUB_WORKSPACE/liblbug");$(cygpath -w "$GITHUB_WORKSPACE/vcpkg_installed/x64-windows/bin");$PATH" export PKG_CONFIG_PATH="$GITHUB_WORKSPACE/vcpkg_installed/x64-windows/lib/pkgconfig" export CXXFLAGS="/std:c++20" else