From e8635b6ece1bdccdf69bfca5f2ccc64216117fa6 Mon Sep 17 00:00:00 2001 From: Jan Baudisch Date: Tue, 9 Jul 2024 18:49:56 +0200 Subject: [PATCH] feat(ffi): initial version --- .github/workflows/CI.yaml | 28 +- .github/workflows/Kotlin.yaml | 74 ++++ .github/workflows/Pages.yaml | 23 + .github/workflows/Python.yaml | 32 ++ .gitignore | 34 +- Cargo.lock | 398 ++++++++++++++++++ Cargo.toml | 9 +- bindings/README.md | 14 + bindings/kotlin/README.md | 78 ++++ bindings/kotlin/build.gradle.kts | 117 +++++ bindings/kotlin/settings.gradle.kts | 1 + .../kotlin/de/softvare/ddnnife/java-utils.kt | 44 ++ bindings/kotlin/src/test/kotlin/Ddnnf.kt | 77 ++++ bindings/python/pyproject.toml | 11 + bindings/python/test/test_ddnnf.py | 22 + ddnnife/Cargo.toml | 5 + ddnnife/src/ddnnf.rs | 45 +- ddnnife/src/ddnnf/anomalies/atomic_sets.rs | 2 + ddnnife/src/ddnnf/anomalies/core.rs | 45 +- .../src/ddnnf/anomalies/t_wise_sampling.rs | 2 + .../ddnnf/anomalies/t_wise_sampling/config.rs | 1 + .../ddnnf/anomalies/t_wise_sampling/sample.rs | 1 + .../t_wise_sampling/sample_merger.rs | 2 + .../t_wise_sampling/sampling_result.rs | 1 + ddnnife/src/ddnnf/node.rs | 6 +- ddnnife/src/ddnnf/stream.rs | 38 +- ddnnife/src/ffi.rs | 194 +++++++++ ddnnife/src/lib.rs | 13 +- ddnnife/src/util.rs | 28 ++ ddnnife/uniffi.toml | 22 + ddnnife_bin/Cargo.toml | 13 +- ddnnife_bindgen/Cargo.toml | 15 + ddnnife_bindgen/src/main.rs | 3 + ddnnife_dhone/Cargo.toml | 6 +- deny.toml | 5 +- flake.lock | 26 +- flake.nix | 211 ++++------ nix/ddnnife.nix | 171 ++++++++ nix/rust.nix | 25 ++ 39 files changed, 1611 insertions(+), 231 deletions(-) create mode 100644 .github/workflows/Kotlin.yaml create mode 100644 .github/workflows/Pages.yaml create mode 100644 .github/workflows/Python.yaml create mode 100644 bindings/README.md create mode 100644 bindings/kotlin/README.md create mode 100644 bindings/kotlin/build.gradle.kts create mode 100644 bindings/kotlin/settings.gradle.kts create mode 100644 bindings/kotlin/src/main/kotlin/de/softvare/ddnnife/java-utils.kt create mode 100644 bindings/kotlin/src/test/kotlin/Ddnnf.kt create mode 100644 bindings/python/pyproject.toml create mode 100644 bindings/python/test/test_ddnnf.py create mode 100644 ddnnife/src/ffi.rs create mode 100644 ddnnife/uniffi.toml create mode 100644 ddnnife_bindgen/Cargo.toml create mode 100644 ddnnife_bindgen/src/main.rs create mode 100644 nix/ddnnife.nix create mode 100644 nix/rust.nix diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index b0cb7d8..838ca74 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -30,13 +30,19 @@ jobs: variant: - flake: ddnnife artifact: '' - - flake: bundled-d4 + - flake: ddnnife-d4-bundled artifact: '-d4' + exclude: + - target: { double: x86_64-linux } + variant: { flake: ddnnife } include: + - target: { double: x86_64-linux, runner: ubuntu-latest } + variant: { flake: ddnnife-static, artifact: '' } + docs: true - target: { double: x86_64-windows, runner: ubuntu-latest } variant: { flake: ddnnife-windows, artifact: '' } - target: { double: x86_64-windows, runner: ubuntu-latest } - variant: { flake: bundled-d4-windows, artifact: '-d4' } + variant: { flake: ddnnife-windows-d4-bundled, artifact: '-d4' } runs-on: ${{ matrix.target.runner }} steps: - name: Checkout @@ -48,7 +54,7 @@ jobs: - name: Build run: nix build -L .#${{ matrix.variant.flake }} - name: Set interpreter - if: ${{ matrix.target.double == 'x86_64-linux' && matrix.variant.flake == 'bundled-d4' }} + if: ${{ matrix.target.double == 'x86_64-linux' && matrix.variant.flake == 'ddnnife-d4-bundled' }} run: | cp -rL result output rm -rf result @@ -61,3 +67,19 @@ jobs: with: name: ddnnife-${{ matrix.target.double }}${{ matrix.variant.artifact }} path: result + - name: Docs + if: ${{ matrix.target.docs }} + run: | + nix build .#documentation + mkdir docs + cp -rL result/share/doc docs/rust + - name: Upload (docs) + if: ${{ matrix.target.docs }} + uses: actions/upload-artifact@v4 + with: + name: pages-rust + path: docs + Pages: + if: github.ref == 'refs/heads/main' + needs: Build + uses: ./.github/workflows/Pages.yaml diff --git a/.github/workflows/Kotlin.yaml b/.github/workflows/Kotlin.yaml new file mode 100644 index 0000000..a145eae --- /dev/null +++ b/.github/workflows/Kotlin.yaml @@ -0,0 +1,74 @@ +name: Kotlin + +on: + - push + +jobs: + Build: + strategy: + fail-fast: false + matrix: + target: + - double: x86_64-linux + jna: linux-x86-64 + runner: ubuntu-24.04 + docs: true + - double: x86_64-darwin + jna: darwin-x86-64 + runner: macos-13 + - double: aarch64-darwin + jna: darwin-aarch64 + runner: macos-latest + variant: + - '' + - '-d4' + exclude: + - target: { double: x86_64-darwin } + variant: '-d4' + runs-on: ${{ matrix.target.runner }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Nix + uses: DeterminateSystems/nix-installer-action@v10 + - name: Cache + uses: DeterminateSystems/magic-nix-cache-action@v4 + - name: Build (bindgen) + run: | + nix build -L .#bindgen + cp -L result/bin/uniffi-bindgen . + - name: Build (library) + run: | + nix build -L .#libddnnife${{ matrix.variant }} + mkdir -p libraries/${{ matrix.target.jna }} + cp -L result/lib/*ddnnife* libraries/${{ matrix.target.jna }}/ + - name: Build + run: | + cd bindings/kotlin + gradle shadowJar --no-daemon -Plibraries=../../libraries -Pbindgen=../../uniffi-bindgen + - name: Test + run: | + cd bindings/kotlin + gradle test --no-daemon -Plibraries=../../libraries -Pbindgen=../../uniffi-bindgen + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ddnnife-kotlin-${{ matrix.target.double }}${{ matrix.variant }} + path: bindings/kotlin/build/libs + - name: Docs + if: ${{ matrix.target.docs && matrix.variant == '' }} + run: | + cd bindings/kotlin + gradle dokkaHtml --no-daemon -Plibraries=../../libraries -Pbindgen=../../uniffi-bindgen + mkdir docs + mv build/dokka/html docs/kotlin + - name: Upload (docs) + if: ${{ matrix.target.docs && matrix.variant == '' }} + uses: actions/upload-artifact@v4 + with: + name: pages-kotlin + path: bindings/kotlin/docs + Pages: + if: github.ref == 'refs/heads/main' + needs: Build + uses: ./.github/workflows/Pages.yaml diff --git a/.github/workflows/Pages.yaml b/.github/workflows/Pages.yaml new file mode 100644 index 0000000..9bb9ed4 --- /dev/null +++ b/.github/workflows/Pages.yaml @@ -0,0 +1,23 @@ +name: Pages + +on: workflow_call + +jobs: + Deploy: + permissions: + pages: write + id-token: write + runs-on: ubuntu-latest + steps: + - name: Download + uses: actions/download-artifact@v4 + with: + path: pages + pattern: pages-* + merge-multiple: true + - name: Upload + uses: actions/upload-pages-artifact@v3 + with: + path: pages + - name: Deploy + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/Python.yaml b/.github/workflows/Python.yaml new file mode 100644 index 0000000..a9b12c7 --- /dev/null +++ b/.github/workflows/Python.yaml @@ -0,0 +1,32 @@ +name: Python + +on: + - push + +jobs: + Build: + strategy: + fail-fast: false + matrix: + target: + - double: x86_64-linux + runner: ubuntu-latest + - double: x86_64-darwin + runner: macos-13 + - double: aarch64-darwin + runner: macos-latest + runs-on: ${{ matrix.target.runner }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Nix + uses: DeterminateSystems/nix-installer-action@v10 + - name: Cache + uses: DeterminateSystems/magic-nix-cache-action@v4 + - name: Build + run: nix build -L .#python + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ddnnife-python-${{ matrix.target.double }} + path: result diff --git a/.gitignore b/.gitignore index 4549d1d..d37a55a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ - -# These are backup files generated by rustfmt +# Rust +/target **/*.rs.bk # Local files for testing, debugging and analysing the code @@ -33,14 +30,25 @@ valcom *-mermaid.md *.svg -# vsc -.idea -.vscode/ +# Nix +result + +# Kotlin +bindings/kotlin/src/main/kotlin/de/softvare/ddnnife/ddnnife.kt +bindings/kotlin/.gradle +bindings/kotlin/gradle +bindings/kotlin/gradlew +bindings/kotlin/gradlew.bat +bindings/kotlin/build -# intellij -ideas/ +# Python +.venv +__pycache__ + +# IDEs +.idea *.iml +.vscode -# d4 repo and binary -d4v2 -d4v2.bin \ No newline at end of file +# macOS +.DS_Store diff --git a/Cargo.lock b/Cargo.lock index 0954629..9a700b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,53 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + [[package]] name = "assert_cmd" version = "2.0.14" @@ -72,6 +119,24 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -101,6 +166,44 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "camino" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cc" version = "1.0.100" @@ -285,6 +388,7 @@ dependencies = [ "serial_test", "streaming-iterator", "tempfile", + "uniffi", "workctl", ] @@ -297,6 +401,13 @@ dependencies = [ "mimalloc", ] +[[package]] +name = "ddnnife_bindgen" +version = "0.7.0" +dependencies = [ + "uniffi", +] + [[package]] name = "ddnnife_dhone" version = "0.7.0" @@ -358,6 +469,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "funty" version = "2.0.0" @@ -458,6 +578,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -584,6 +715,22 @@ dependencies = [ "libmimalloc-sys", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -709,6 +856,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "petgraph" version = "0.6.5" @@ -737,6 +890,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -889,6 +1048,35 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + [[package]] name = "serde" version = "1.0.203" @@ -909,6 +1097,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serial_test" version = "2.0.0" @@ -934,6 +1133,12 @@ dependencies = [ "syn", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -949,6 +1154,18 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "streaming-iterator" version = "0.1.9" @@ -1005,6 +1222,53 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1017,12 +1281,137 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "uniffi" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31bff6daf87277a9014bcdefbc2842b0553392919d1096843c5aad899ca4588" +dependencies = [ + "anyhow", + "camino", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96061d7e01b185aa405f7c9b134741ab3e50cc6796a47d6fd8ab9a5364b5feed" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "once_cell", + "paste", + "serde", + "textwrap", + "toml", + "uniffi_meta", + "uniffi_testing", + "uniffi_udl", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcfa22f55829d3aaa7acfb1c5150224188fe0f27c59a8a3eddcaa24d1ffbe58" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "uniffi_core" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3210d57d6ab6065ab47a2898dacdb7c606fd6a4156196831fa3bf82e34ac58a6" +dependencies = [ + "anyhow", + "bytes", + "camino", + "log", + "once_cell", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58691741080935437dc862122e68d7414432a11824ac1137868de46181a0bd2" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7663eacdbd9fbf4a88907ddcfe2e6fa85838eb6dc2418a7d91eebb3786f8e20b" +dependencies = [ + "anyhow", + "bytes", + "siphasher", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f922465f7566f25f8fe766920205fdfa9a3fcdc209c6bfb7557f0b5bf45b04dd" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef408229a3a407fafa4c36dc4f6ece78a6fb258ab28d2b64bddd49c8cb680f6" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -1038,6 +1427,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + [[package]] name = "winapi-util" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 63c655f..3c1aa63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,18 @@ members = [ "ddnnife", "ddnnife_bin", - "ddnnife_dhone" + "ddnnife_bindgen", + "ddnnife_dhone", ] resolver = "2" +[workspace.dependencies] +clap = { version = "4.5", features = ["derive"] } +ddnnife = { path = "ddnnife" } +mimalloc = "0.1" +uniffi = { version = "0.28" } + [profile.release] lto = true codegen-units = 1 diff --git a/bindings/README.md b/bindings/README.md new file mode 100644 index 0000000..bad4217 --- /dev/null +++ b/bindings/README.md @@ -0,0 +1,14 @@ +# Bindings + +`ddnnife` bindings are provided via [`uniffi`][uniffi]. +Each language binding is located in its own subdirectory and should stay separated from the Rust codebase. + +For building instructions, see the specific binding's README. + +## `ddnnife_bindgen` + +This is a version of `uniffi-bindgen` specific to this project. +Its task is to actually generate the bindings to the Rust library. +The binary of this crate is called `uniffi-bindgen` to work with build tools. + +[uniffi]: https://mozilla.github.io/uniffi-rs diff --git a/bindings/kotlin/README.md b/bindings/kotlin/README.md new file mode 100644 index 0000000..5779271 --- /dev/null +++ b/bindings/kotlin/README.md @@ -0,0 +1,78 @@ +# Kotlin bindings for `ddnnife` + +> [!IMPORTANT] +> A note on `d4`: +> As the Rust crate, these bindings can also work with `d4` to provide a CNF to d-DNNF compiler. +> The JARs built by this project only contain the library. +> To use the `d4` variant, the dependencies need to present. + +## Usage + +### Java Utils + +Some Kotlin methods as `Ddnnf.fromFile` don't have a direct translation to Java, because of their parameter types. +Instead, the `JavaUtils` class provides access to these methods with Java parameters. +When using such methods, it needs to be ensured that the parameters have correct values as they might throw exceptions otherwise. + +## Build + +### Requirements + +- [Rust][rust] (or prebuilt `ddnnife` libraries and `ddnnife-bindgen`) +- [Gradle][gradle] +- [ktlint][ktlint] (optional) + +There are two ways to build the bindings: +Locally by building the Rust library first, or by passing a directory containing the pre-built libraries. + +### Locally + +``` +gradle build +``` + +This will invoke a Rust build of `ddnnife`, generate the Kotlin bindings by running `ddnnife-bindgen` via cargo. + +### Pre-built + +With a directory containing the pre-built libraries for each platform in the following layout (at least the host platform is required): + +``` +/path/to/prebuilt/libraries +├── darwin-aarch64 +│ └── libddnnife.dylib +├── darwin-x86-64 +│ └── libddnnife.dylib +├── linux-aarch64 +│ └── libddnnife.so +└── linux-x86-64 + └── libddnnife.so +``` + +... running the following will build the bindings without a Rust build of `ddnnife` + +``` +gradle build -Plibraries=/path/to/prebuilt/libraries +``` + +### `uniffi-bindgen` + +By default, the Gradle build will run the corresponding crate in this workspace via cargo. +To prevent this, a command/path to the `uniffi-bindgen` binary can be provided: + +``` +gradle build -Pbindgen=/path/to/uniffi-bindgen +``` + +Using this together with the pre-built libraries a Gradle built without any Rust invocation is possible. + +### Generated sources + +The `de.softvare.ddnnife` package in `src/main/kotlin` is generated by `uniffi` but kept in this location for better compliance with tools like [Dokka][dokka]. +It will be auto-generated by Gradle and should not be committed into Git. + +[jna]: https://github.com/java-native-access/jna +[rust]: https://www.rust-lang.org +[gradle]: https://gradle.org +[ktlint]: https://github.com/pinterest/ktlint +[dokka]: https://kotlinlang.org/docs/dokka-introduction.html diff --git a/bindings/kotlin/build.gradle.kts b/bindings/kotlin/build.gradle.kts new file mode 100644 index 0000000..0d71b50 --- /dev/null +++ b/bindings/kotlin/build.gradle.kts @@ -0,0 +1,117 @@ +import com.sun.jna.Platform +import org.gradle.internal.os.OperatingSystem + +group = "de.softvare" +version = "0.7.0" + +plugins { + java + kotlin("jvm") version "2.0.0" + id("org.jetbrains.dokka") version "1.9.20" + id("com.github.johnrengelman.shadow") version "8.1.1" +} + +kotlin { + jvmToolchain(17) +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("net.java.dev.jna:jna:5.14.0") + testImplementation(kotlin("test")) +} + +buildscript { + dependencies { + classpath("net.java.dev.jna:jna:5.14.0") + } +} + +// Directory structure for generated files. +val generatedResources = "${layout.buildDirectory.get()}/main/resources" + +// When no library folder is passed, we only target the current system and directly use Rust to build the library. +val onlyCurrent = !hasProperty("libraries") + +// Otherwise, extract the path to the pre-built libraries. +var librariesPath = "" +if (!onlyCurrent) { + librariesPath = property("libraries").toString() +} + +// OS specific directories for the native library. +// This only covers the current platform used for generation. +val os: OperatingSystem = OperatingSystem.current() +val resourcePrefix = Platform.RESOURCE_PREFIX; +val libraryDest = "${generatedResources}/${resourcePrefix}" +val libraryName: String = os.getSharedLibraryName("ddnnife") + +// The bindgen tool can be passed via the `bindgen` property, otherwise we invoke it via cargo. +val bindgen = if (hasProperty("bindgen")) { + listOf(property("bindgen").toString()) +} else { + listOf("cargo", "run", "--bin", "uniffi-bindgen") +} + +tasks.register("nativeLibrary") { + group = "Build" + description = "Copies the native library." + + if (onlyCurrent) { + from("../../target/release/${libraryName}") + into(libraryDest) + } else { + from(librariesPath) + into(generatedResources) + } +} + +tasks.processResources { + dependsOn("nativeLibrary") +} + +// Skip building the Rust library in case its path was given. +if (onlyCurrent) { + tasks.register("buildRust") { + group = "Build" + description = "Compiles the Rust crate." + commandLine("cargo", "build", "--release", "--package", "ddnnife", "--features", "uniffi") + } + + tasks.named("nativeLibrary") { + dependsOn("buildRust") + } +} + +tasks.register("generateBindings") { + group = "Build" + description = "Generates the Kotlin uniffi bindings for the Rust crate." + commandLine(bindgen) + args("generate", "--language", "kotlin", "--out-dir", "${layout.projectDirectory}/src/main/kotlin", "--library", "${libraryDest}/${libraryName}", "--no-format") + + dependsOn("nativeLibrary") +} + +tasks.compileKotlin { + dependsOn("generateBindings") +} + +tasks.test { + useJUnitPlatform() + + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + } +} + +sourceSets { + main { + resources { + srcDir(generatedResources) + } + } +} diff --git a/bindings/kotlin/settings.gradle.kts b/bindings/kotlin/settings.gradle.kts new file mode 100644 index 0000000..8a60b1f --- /dev/null +++ b/bindings/kotlin/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "ddnnife" diff --git a/bindings/kotlin/src/main/kotlin/de/softvare/ddnnife/java-utils.kt b/bindings/kotlin/src/main/kotlin/de/softvare/ddnnife/java-utils.kt new file mode 100644 index 0000000..d6f27d4 --- /dev/null +++ b/bindings/kotlin/src/main/kotlin/de/softvare/ddnnife/java-utils.kt @@ -0,0 +1,44 @@ +@file:JvmName("JavaUtils") + +// This files contains code for the Kotlin -> Java interop. + +package de.softvare.ddnnife + +/** + * Loads a d-DNNF from file. + * + * @param path Where to load the d-DNNF from. + * @param features How many features the corresponding model has. + * Can be `null` in which case this will be determined by compilation or from building the d-DNNF. + * */ +fun ddnnfFromFile(path: String, features: Int?): Ddnnf { + if (features != null) { + require(features >= 0) { "Features amount must be positive." } + } + + return Ddnnf.fromFile(path, features?.toUInt()) +} + +fun toUInt(i: Int): UInt { + require(i >= 0) { "Integer must be positive." } + return i.toUInt() +} + +fun enumerate(ddnnf: DdnnfMut, assumptions: List, amount: Int): List> { + require(amount >= 0) { "Amount must be positive." } + return ddnnf.enumerate(assumptions, amount.toULong()) +} + +fun random(ddnnf: DdnnfMut, assumptions: List, amount: Int, seed: Int): List> { + require(amount >= 0) { "Amount must be positive." } + return ddnnf.random(assumptions, amount.toULong(), seed.toULong()) +} + +fun atomicSets(ddnnf: DdnnfMut, candidates: List, assumptions: List, cross: Boolean): List> { + val candidatesUInt = candidates.map { candidate -> + require(candidate >= 0) { "Candidates must be positive." } + candidate.toUInt() + } + + return ddnnf.atomicSets(candidatesUInt, assumptions, cross) +} diff --git a/bindings/kotlin/src/test/kotlin/Ddnnf.kt b/bindings/kotlin/src/test/kotlin/Ddnnf.kt new file mode 100644 index 0000000..072b4b4 --- /dev/null +++ b/bindings/kotlin/src/test/kotlin/Ddnnf.kt @@ -0,0 +1,77 @@ +import de.softvare.ddnnife.Ddnnf +import de.softvare.ddnnife.SamplingResult +import java.math.BigInteger +import kotlin.test.assertEquals +import kotlin.test.Test +import kotlin.test.fail + +internal class Ddnnf { + private val ddnnf = Ddnnf.fromFile("../../example_input/busybox-1.18.0_c2d.nnf", null) + private val features = 854 + + @Test + fun sat() { + val ddnnf = ddnnf.asMut() + assert(ddnnf.isSat(emptyList())) + assert(ddnnf.isSat(listOf(-2))) + assert(!ddnnf.isSat(listOf(1))) + } + + @Test + fun count() { + // Only the root count. + val count = ddnnf.rc() + val expected = BigInteger("2061138519356781760670618805653750167349287991336595876373542198990734653489713239449032049664199494301454199336000050382457451123894821886472278234849758979132037884598159833615564800000000000000000000") + assertEquals(count, expected) + + // The count for a given assumption. + val ddnnf = ddnnf.asMut() + assertEquals(ddnnf.count(emptyList()), count) + assertEquals(ddnnf.count(listOf(1)), BigInteger("0")) + } + + @Test + fun coreAndDead() { + val both = ddnnf.getCore() + assertEquals(41, both.size) + + val ddnnf = ddnnf.asMut() + val core = ddnnf.core(emptyList()) + assertEquals(23, core.size) + + val dead = ddnnf.dead(emptyList()) + assertEquals(18, dead.size) + } + + @Test + fun enumerate() { + val configs = ddnnf.asMut().enumerate(emptyList(), 1u) + assertEquals(1, configs.size) + assertEquals(features, configs[0].size) + } + + @Test + fun random() { + val configs = ddnnf.asMut().random(emptyList(), 2u, 42u) + assertEquals(2, configs.size) + assertEquals(features, configs[0].size) + } + + @Test + fun atomicSets() { + val atomicSets = ddnnf.asMut().atomicSets(null, listOf(1), true) + assertEquals(features, atomicSets[0].size) + } + + @Test + fun tWise() { + when(val sample = ddnnf.sampleTWise(1u)) { + is SamplingResult.ResultWithSample -> { + assertEquals(features, sample.v1.vars.size) + } + else -> { + fail("T-wise sample is invalid.") + } + } + } +} diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml new file mode 100644 index 0000000..c1c138b --- /dev/null +++ b/bindings/python/pyproject.toml @@ -0,0 +1,11 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[tool.maturin] +profile = "release" +bindings = "uniffi" +features = ["uniffi"] +manifest-path = "../../ddnnife/Cargo.toml" +frozen = true +strip = true diff --git a/bindings/python/test/test_ddnnf.py b/bindings/python/test/test_ddnnf.py new file mode 100644 index 0000000..6d43eab --- /dev/null +++ b/bindings/python/test/test_ddnnf.py @@ -0,0 +1,22 @@ +from ddnnife import Ddnnf + +ddnnf = Ddnnf.from_file("../../example_input/busybox-1.18.0_c2d.nnf", None) + + +def test_count(): + count = ddnnf.rc() + assert count == 2061138519356781760670618805653750167349287991336595876373542198990734653489713239449032049664199494301454199336000050382457451123894821886472278234849758979132037884598159833615564800000000000000000000 + + +def test_core(): + core = ddnnf.get_core() + assert len(core) == 41 + + +def test_atomic_sets(): + atomic_sets = ddnnf.as_mut().atomic_sets(None, [1], True) + assert len(atomic_sets[0]) == 854 + + +def test_t_wise(): + sample = ddnnf.sample_t_wise(1) diff --git a/ddnnife/Cargo.toml b/ddnnife/Cargo.toml index 76d94e3..5f054f4 100644 --- a/ddnnife/Cargo.toml +++ b/ddnnife/Cargo.toml @@ -10,6 +10,10 @@ workspace = ".." [features] d4 = ["dep:d4-oxide"] deterministic = [] +uniffi = ["dep:uniffi"] + +[lib] +crate-type = ["lib", "cdylib"] [dependencies] bitvec = "1.0" @@ -25,6 +29,7 @@ rand_distr = "0.4" rand_pcg = "0.3" streaming-iterator = "0.1" tempfile = "3.10" +uniffi = { workspace = true, optional = true } workctl = "0.2" [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))'.dependencies] diff --git a/ddnnife/src/ddnnf.rs b/ddnnife/src/ddnnf.rs index 5c93e89..455bc68 100644 --- a/ddnnife/src/ddnnf.rs +++ b/ddnnife/src/ddnnf.rs @@ -6,19 +6,19 @@ pub mod multiple_queries; pub mod node; pub mod stream; -use std::collections::{BTreeSet, HashMap, HashSet}; - +use self::{clause_cache::ClauseCache, node::Node}; +use crate::parser::build_ddnnf; use itertools::Either; use num::BigInt; - -use self::{clause_cache::ClauseCache, node::Node}; +use std::collections::{BTreeSet, HashMap, HashSet}; type Clause = BTreeSet; type ClauseSet = BTreeSet; type EditOperation = (Vec, Vec); -#[derive(Clone, Debug)] /// A Ddnnf holds all the nodes as a vector, also includes meta data and further information that is used for optimations +#[derive(Clone, Debug)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Object))] pub struct Ddnnf { /// The actual nodes of the d-DNNF in postorder pub nodes: Vec, @@ -36,7 +36,9 @@ pub struct Ddnnf { pub max_worker: u16, } +#[cfg_attr(feature = "uniffi", uniffi::export)] impl Default for Ddnnf { + #[cfg_attr(feature = "uniffi", uniffi::constructor)] fn default() -> Self { Ddnnf { nodes: Vec::new(), @@ -51,6 +53,31 @@ impl Default for Ddnnf { } } +#[cfg_attr(feature = "uniffi", uniffi::export)] +impl Ddnnf { + /// Loads a d-DNNF from file. + #[cfg_attr(feature = "uniffi", uniffi::constructor)] + fn from_file(path: String, features: Option) -> Self { + build_ddnnf(&path.clone(), features) + } + + /// Returns the current count of the root node in the d-DNNF. + /// + /// This value is the same during all computations. + #[cfg_attr(feature = "uniffi", uniffi::method)] + pub fn rc(&self) -> BigInt { + self.nodes[self.nodes.len() - 1].count.clone() + } + + /// Returns the core features of this d-DNNF. + /// + /// This is only calculated once at creation of the d-DNNF. + #[cfg_attr(feature = "uniffi", uniffi::method)] + pub fn get_core(&self) -> HashSet { + self.core.clone() + } +} + impl Ddnnf { /// Creates a new ddnnf including dead and core features pub fn new( @@ -70,7 +97,7 @@ impl Ddnnf { number_of_variables, max_worker: 4, }; - ddnnf.get_core(); + ddnnf.calculate_core(); if let Some(c) = clauses { ddnnf.update_cached_state(Either::Right(c), Some(number_of_variables)); } @@ -150,12 +177,6 @@ impl Ddnnf { } } - // Returns the current count of the root node in the ddnnf. - // That value is the same during all computations - pub fn rc(&self) -> BigInt { - self.nodes[self.nodes.len() - 1].count.clone() - } - // Returns the current temp count of the root node in the ddnnf. // That value is changed during computations fn rt(&self) -> BigInt { diff --git a/ddnnife/src/ddnnf/anomalies/atomic_sets.rs b/ddnnife/src/ddnnf/anomalies/atomic_sets.rs index be072bc..1a00d63 100644 --- a/ddnnife/src/ddnnf/anomalies/atomic_sets.rs +++ b/ddnnife/src/ddnnf/anomalies/atomic_sets.rs @@ -200,7 +200,9 @@ impl Ddnnf { } subsets } +} +impl Ddnnf { /// Computes the signs of the features in multiple uniform random samples. /// Each of the features is represented by an BitArray holds as many entries as random samples /// with a 0 indicating that the feature occurs negated and a 1 indicating the feature occurs affirmed. diff --git a/ddnnife/src/ddnnf/anomalies/core.rs b/ddnnife/src/ddnnf/anomalies/core.rs index 0a5132c..1f82bde 100644 --- a/ddnnife/src/ddnnf/anomalies/core.rs +++ b/ddnnife/src/ddnnf/anomalies/core.rs @@ -1,12 +1,12 @@ -use std::collections::HashSet; - use crate::Ddnnf; +use num::Zero; +use std::collections::HashSet; impl Ddnnf { /// Computes all dead and core features. /// A feature is a core feature iff there exists only the positiv occurence of that feature. /// A feature is a dead feature iff there exists only the negativ occurence of that feature. - pub(crate) fn get_core(&mut self) { + pub(crate) fn calculate_core(&mut self) { self.core = (-(self.number_of_variables as i32)..=self.number_of_variables as i32) .filter(|f| self.literals.contains_key(f) && !self.literals.contains_key(&-f)) .collect::>() @@ -45,4 +45,43 @@ impl Ddnnf { // if there is an included dead or an excluded core feature features.iter().any(|f| self.makes_query_unsat(f)) } + + /// Calculates the core and dead features for a given assumption. + /// + /// The return values are not deduplicated, they might be put into a `HashSet`. + pub fn core_dead_with_assumptions(self: &mut Ddnnf, assumptions: &[i32]) -> Vec { + if assumptions.is_empty() { + return self.core.iter().copied().collect(); + } + + let mut assumptions = assumptions.to_vec(); + let mut core = Vec::new(); + let reference = self.execute_query(&assumptions); + + for i in 1_i32..=self.number_of_variables as i32 { + assumptions.push(i); + let inter = self.execute_query(&assumptions); + if reference == inter { + core.push(i); + } + if inter.is_zero() { + core.push(-i); + } + assumptions.pop(); + } + + core + } + + pub fn core_with_assumptions(self: &mut Ddnnf, assumptions: &[i32]) -> Vec { + let mut features = self.core_dead_with_assumptions(assumptions); + features.retain(|feature| feature.is_positive()); + features + } + + pub fn dead_with_assumptions(self: &mut Ddnnf, assumptions: &[i32]) -> Vec { + let mut features = self.core_dead_with_assumptions(assumptions); + features.retain(|feature| feature.is_negative()); + features + } } diff --git a/ddnnife/src/ddnnf/anomalies/t_wise_sampling.rs b/ddnnife/src/ddnnf/anomalies/t_wise_sampling.rs index 1bc0809..ccee64b 100644 --- a/ddnnife/src/ddnnf/anomalies/t_wise_sampling.rs +++ b/ddnnife/src/ddnnf/anomalies/t_wise_sampling.rs @@ -19,8 +19,10 @@ use std::path::Path; use std::{fs, io, iter}; use t_wise_sampler::TWiseSampler; +#[cfg_attr(feature = "uniffi", uniffi::export)] impl Ddnnf { /// Generates samples so that all t-wise interactions between literals are covered. + #[cfg_attr(feature = "uniffi", uniffi::method)] pub fn sample_t_wise(&self, t: usize) -> SamplingResult { // Setup everything needed for the sampling process. let sat_solver = SatWrapper::new(self); diff --git a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/config.rs b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/config.rs index 1c162aa..a2f2d95 100644 --- a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/config.rs +++ b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/config.rs @@ -3,6 +3,7 @@ use crate::util::format_vec; use std::fmt::Display; /// Represents a (partial) configuration +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(Debug, Clone, Eq)] pub struct Config { /// A vector of selected features (positive values) and deselected features (negative values) diff --git a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample.rs b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample.rs index 8e2170c..a7cc709 100644 --- a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample.rs +++ b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample.rs @@ -7,6 +7,7 @@ use std::iter; /// The sample differentiates between complete and partial configs. /// A config is complete (in the context of this sample) if it contains all variables this sample /// defines. Otherwise the config is partial. +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Sample { /// Configs that contain all variables of this sample diff --git a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger.rs b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger.rs index 0162ac5..9530a96 100644 --- a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger.rs +++ b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sample_merger.rs @@ -40,6 +40,7 @@ pub(super) trait AndMerger: SampleMerger {} pub(super) trait OrMerger: SampleMerger {} /// A simple [AndMerger] that just builds all valid configs +#[allow(dead_code)] #[derive(Debug, Clone, Copy)] pub(super) struct DummyAndMerger<'a> { ddnnf: &'a Ddnnf, @@ -77,6 +78,7 @@ impl SampleMerger for DummyAndMerger<'_> { impl AndMerger for DummyAndMerger<'_> {} /// A simple [OrMerger] that just builds all valid configs +#[allow(dead_code)] #[derive(Debug, Clone, Copy)] pub(super) struct DummyOrMerger {} diff --git a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sampling_result.rs b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sampling_result.rs index b5220b6..f185eb8 100644 --- a/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sampling_result.rs +++ b/ddnnife/src/ddnnf/anomalies/t_wise_sampling/sampling_result.rs @@ -3,6 +3,7 @@ use crate::util::format_vec_separated_by; use std::fmt; /// An abstraction over the result of sampling as it might be invalid or empty. +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[derive(Debug, Clone, PartialEq, Eq)] pub enum SamplingResult { /// An empty result that is *valid* (a regular sample containing 0 configurations). diff --git a/ddnnife/src/ddnnf/node.rs b/ddnnife/src/ddnnf/node.rs index 5c38a05..bb35d3b 100644 --- a/ddnnife/src/ddnnf/node.rs +++ b/ddnnife/src/ddnnf/node.rs @@ -1,8 +1,9 @@ use num::BigInt; use NodeType::{And, False, Literal, Or, True}; -#[derive(Debug, Clone, PartialEq)] /// Represents all types of Nodes with its different parts +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct Node { pub(crate) marker: bool, /// The cardinality of the node for the cardinality of a feature model @@ -17,8 +18,9 @@ pub struct Node { pub ntype: NodeType, } -#[derive(Debug, Clone, PartialEq)] /// The Type of the Node declares how we handle the computation for the different types of cardinalities +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[derive(Debug, Clone, PartialEq)] pub enum NodeType { /// The cardinality of an And node is always the product of its childs And { children: Vec }, diff --git a/ddnnife/src/ddnnf/stream.rs b/ddnnife/src/ddnnf/stream.rs index d241b01..b3e6e07 100644 --- a/ddnnife/src/ddnnf/stream.rs +++ b/ddnnife/src/ddnnf/stream.rs @@ -1,7 +1,6 @@ use std::cmp::Reverse; use std::collections::{BTreeSet, BinaryHeap, HashSet}; use std::io::BufRead; -use std::iter::FromIterator; use std::path::Path; use std::process::exit; use std::sync::atomic::{AtomicBool, Ordering}; @@ -16,7 +15,7 @@ use nom::character::complete::{char, digit1}; use nom::combinator::{map_res, opt, recognize}; use nom::sequence::{pair, tuple}; use nom::IResult; -use num::{BigInt, ToPrimitive, Zero}; +use num::{BigInt, ToPrimitive}; use workctl::WorkQueue; use crate::parser::persisting::{write_cnf_to_file, write_ddnnf_to_file}; @@ -332,30 +331,15 @@ impl Ddnnf { let with_cf = Ddnnf::execute_query(d, assumptions); if with_cf == without_cf { - Some(could_be_core.to_string()) + return Some(could_be_core.to_string()); } else { - None + return None; } - } else if assumptions.is_empty() { - let mut core = Vec::from_iter(&d.core); - core.sort_by_key(|a| a.abs()); - Some(format_vec(core.iter())) - } else { - let mut core = Vec::new(); - let reference = Ddnnf::execute_query(d, assumptions); - for i in 1_i32..=d.number_of_variables as i32 { - assumptions.push(i); - let inter = Ddnnf::execute_query(d, assumptions); - if reference == inter { - core.push(i); - } - if inter.is_zero() { - core.push(-i); - } - assumptions.pop(); - } - Some(format_vec(core.iter())) } + + Some(format_vec( + d.core_dead_with_assumptions(assumptions).iter().sorted(), + )) }, self, &mut params, @@ -694,10 +678,10 @@ mod test { String::from("1 2 6 10 15 19 25 31 40"), vp9.handle_stream_msg("core assumptions 1") ); - assert!( - // count p 1 2 3 == 0 => all features are core under that assumption - auto1.handle_stream_msg("core a 1 2 3").split(' ').count() - == (auto1.number_of_variables * 2) as usize + + assert_eq!( + auto1.handle_stream_msg("core a 1 2 3").split(' ').count(), + (auto1.number_of_variables * 2) as usize ); assert_eq!( diff --git a/ddnnife/src/ffi.rs b/ddnnife/src/ffi.rs new file mode 100644 index 0000000..2e8eb5a --- /dev/null +++ b/ddnnife/src/ffi.rs @@ -0,0 +1,194 @@ +use crate::parser::persisting::write_ddnnf_to_file; +use crate::util; +use crate::{Ddnnf, UniffiCustomTypeConverter}; +use itertools::Itertools; +use num::BigInt; +use std::collections::HashSet; +use std::sync::Mutex; + +uniffi::custom_type!(BigInt, Vec); + +impl UniffiCustomTypeConverter for BigInt { + type Builtin = Vec; + + fn into_custom(value: Self::Builtin) -> uniffi::Result { + Ok(BigInt::from_signed_bytes_be(&value)) + } + + fn from_custom(custom: Self) -> Self::Builtin { + custom.to_signed_bytes_be() + } +} + +uniffi::custom_type!(usize, u64); + +impl UniffiCustomTypeConverter for usize { + type Builtin = u64; + + fn into_custom(value: Self::Builtin) -> uniffi::Result { + Ok(value as usize) + } + + fn from_custom(custom: Self) -> Self::Builtin { + custom as u64 + } +} + +type HashSetu32 = HashSet; +type Vecu32 = Vec; + +uniffi::custom_type!(HashSetu32, Vecu32); + +impl UniffiCustomTypeConverter for HashSet { + type Builtin = Vec; + + fn into_custom(value: Self::Builtin) -> uniffi::Result { + Ok(value.into_iter().collect()) + } + + fn from_custom(custom: Self) -> Self::Builtin { + custom.into_iter().collect() + } +} + +type HashSeti32 = HashSet; +type Veci32 = Vec; + +uniffi::custom_type!(HashSeti32, Veci32); + +impl UniffiCustomTypeConverter for HashSet { + type Builtin = Vec; + + fn into_custom(value: Self::Builtin) -> uniffi::Result { + Ok(value.into_iter().collect()) + } + + fn from_custom(custom: Self) -> Self::Builtin { + custom.into_iter().collect() + } +} + +/// A mutable version of a d-DNNF, required for some computations. +/// +/// This version has thread-safe access to computations requiring mutability. +/// A lock will be managed directly by the library. +/// Converting into and out from the mutable version will create new instances. +#[derive(uniffi::Object)] +pub struct DdnnfMut(pub Mutex); + +#[uniffi::export] +impl Ddnnf { + /// Creates a mutable copy of this d-DNNF. + #[uniffi::method] + fn as_mut(&self) -> DdnnfMut { + DdnnfMut(Mutex::new(self.clone())) + } + + /// Saves this d-DNNF to the given file. + #[uniffi::method] + fn save(&self, path: &str) { + write_ddnnf_to_file(self, path).unwrap(); + } +} + +#[uniffi::export] +impl DdnnfMut { + /// Creates a non-mutable copy of this d-DNNF. + #[uniffi::method] + fn as_ddnnf(&self) -> Ddnnf { + self.0.lock().expect("Failed to lock d-DNNF.").clone() + } + + /// Computes the cardinality of this d-DNNF. + #[uniffi::method] + fn count(&self, assumptions: &[i32]) -> BigInt { + self.0.lock().unwrap().execute_query(assumptions) + } + + /// Computes the cardinality of this d-DNNF for multiple variables. + #[uniffi::method] + fn count_multiple(&self, assumptions: &[i32], variables: &[i32]) -> Vec { + let mut ddnnf = self.0.lock().unwrap(); + util::zip_assumptions_variables(assumptions, variables) + .map(|assumptions| ddnnf.execute_query(&assumptions)) + .collect() + } + + /// Computes whether this d-DNNF is satisfiable. + #[uniffi::method] + fn is_sat(&self, assumptions: &[i32]) -> bool { + self.0.lock().unwrap().sat(assumptions) + } + + /// Computes the core features of this d-DNNF. + #[uniffi::method] + fn core(&self, assumptions: &[i32]) -> Vec { + self.0.lock().unwrap().core_with_assumptions(assumptions) + } + + /// Computes the core features of this d-DNNF for multiple variables. + #[uniffi::method] + fn core_multiple(&self, assumptions: &[i32], variables: &[i32]) -> Vec { + let mut ddnnf = self.0.lock().unwrap(); + util::zip_assumptions_variables(assumptions, variables) + .flat_map(|assumptions| ddnnf.core_with_assumptions(&assumptions).into_iter()) + .sorted() + .dedup() + .collect() + } + + /// Computes the dead features of this d-DNNF. + #[uniffi::method] + fn dead(&self, assumptions: &[i32]) -> Vec { + self.0.lock().unwrap().dead_with_assumptions(assumptions) + } + + /// Computes the dead features of this d-DNNF for multiple variables. + #[uniffi::method] + fn dead_multiple(&self, assumptions: &[i32], variables: &[i32]) -> Vec { + let mut ddnnf = self.0.lock().unwrap(); + util::zip_assumptions_variables(assumptions, variables) + .flat_map(|assumptions| ddnnf.dead_with_assumptions(&assumptions).into_iter()) + .sorted() + .dedup() + .collect() + } + + /// Generates satisfiable configurations for this d-DNNF. + #[uniffi::method] + fn enumerate(&self, assumptions: &[i32], amount: usize) -> Vec> { + let mut assumptions = assumptions.to_vec(); + self.0 + .lock() + .unwrap() + .enumerate(&mut assumptions, amount) + .unwrap_or_default() + } + + /// Generates random satisfiable configurations for this d-DNNF. + #[uniffi::method] + fn random(&self, assumptions: &[i32], amount: usize, seed: u64) -> Vec> { + self.0 + .lock() + .unwrap() + .uniform_random_sampling(assumptions, amount, seed) + .unwrap_or_default() + } + + /// Compute all atomic sets. + /// + /// A group forms an atomic set iff every valid configuration either includes + /// or excludes all members of that atomic set. + #[uniffi::method] + fn atomic_sets( + &self, + candidates: Option>, + assumptions: &[i32], + cross: bool, + ) -> Vec> { + self.0 + .lock() + .unwrap() + .get_atomic_sets(candidates, assumptions, cross) + } +} diff --git a/ddnnife/src/lib.rs b/ddnnife/src/lib.rs index ec649ab..1e29428 100644 --- a/ddnnife/src/lib.rs +++ b/ddnnife/src/lib.rs @@ -1,11 +1,5 @@ -//#![warn(missing_docs)] -#![warn(unused_qualifications)] -//#![deny(unreachable_pub)] -#![deny(deprecated)] -#![deny(missing_copy_implementations)] -#![warn(clippy::disallowed_types)] -#[cfg(all(test, feature = "benchmarks"))] -extern crate test; +#[cfg(feature = "uniffi")] +uniffi::setup_scaffolding!(); pub mod parser; pub mod util; @@ -14,3 +8,6 @@ pub use crate::parser::d4_lexer; pub mod ddnnf; pub use crate::ddnnf::{node::*, Ddnnf}; + +#[cfg(feature = "uniffi")] +mod ffi; diff --git a/ddnnife/src/util.rs b/ddnnife/src/util.rs index 9f879a3..e5bd457 100644 --- a/ddnnife/src/util.rs +++ b/ddnnife/src/util.rs @@ -1,4 +1,5 @@ use rand::Rng; +use std::iter; #[cfg(any(feature = "deterministic", test))] use rand::prelude::{SeedableRng, StdRng}; @@ -40,3 +41,30 @@ pub fn rng() -> impl Rng { pub fn rng() -> impl Rng { thread_rng() } + +pub fn zip_assumptions_variables<'a>( + assumptions: &'a [i32], + variables: &'a [i32], +) -> Box> + 'a> { + if assumptions.is_empty() && variables.is_empty() { + return Box::new(iter::once(Vec::new())); + } + + if assumptions.is_empty() { + return Box::new(variables.iter().copied().map(|variable| vec![variable])); + } + + if variables.is_empty() { + return Box::new(iter::once(assumptions.to_vec())); + } + + Box::new( + iter::repeat(assumptions) + .zip(variables.iter()) + .map(|(assumptions, &variable)| { + let mut assumptions = assumptions.to_vec(); + assumptions.push(variable); + assumptions + }), + ) +} diff --git a/ddnnife/uniffi.toml b/ddnnife/uniffi.toml new file mode 100644 index 0000000..01c947f --- /dev/null +++ b/ddnnife/uniffi.toml @@ -0,0 +1,22 @@ +[bindings.kotlin] +package_name = "de.softvare.ddnnife" + +[bindings.python] +cdylib_name = "ddnnife" + +[bindings.kotlin.custom_types.BigInt] +type_name = "BigInteger" +imports = ["java.math.BigInteger"] +into_custom = "BigInteger({})" +# TODO: does it work? +from_custom = "{}.toByteArray()" + +[bindings.python.custom_types.BigInt] +into_custom = "int.from_bytes({}, 'big')" +# TODO: does it work? +from_custom = "{}.to_bytes()" + +[bindings.kotlin.custom_types.HashSetu32] +type_name = "HashSet" +into_custom = "HashSet({})" +from_custom = "{}.toList()" diff --git a/ddnnife_bin/Cargo.toml b/ddnnife_bin/Cargo.toml index 9753577..841e3e0 100644 --- a/ddnnife_bin/Cargo.toml +++ b/ddnnife_bin/Cargo.toml @@ -7,15 +7,16 @@ edition = "2021" license = "LGPL-3.0-or-later" workspace = ".." -[[bin]] -name = "ddnnife" -path = "src/main.rs" +# FIXME: uniffi-bingen complains about finding the crate name twice, which should not be the case as this is the binary name. +#[[bin]] +#name = "ddnnife" +#path = "src/main.rs" [features] d4 = ["ddnnife/d4"] deterministic = ["ddnnife/deterministic"] [dependencies] -clap = { version = "4.5", features = ["derive"] } -ddnnife = { path = "../ddnnife" } -mimalloc = "0.1" +clap = { workspace = true } +ddnnife = { workspace = true } +mimalloc = { workspace = true } diff --git a/ddnnife_bindgen/Cargo.toml b/ddnnife_bindgen/Cargo.toml new file mode 100644 index 0000000..4649e6c --- /dev/null +++ b/ddnnife_bindgen/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ddnnife_bindgen" +description = "uniffi wrapper for ddnnife ffi bindings" +version = "0.7.0" +authors = ["Heiko Raab ", "Chico Sundermann ", "Jan Baudisch "] +edition = "2021" +license = "LGPL-3.0-or-later" +workspace = ".." + +[[bin]] +name = "uniffi-bindgen" +path = "src/main.rs" + +[dependencies] +uniffi = { workspace = true, features = ["cli"] } diff --git a/ddnnife_bindgen/src/main.rs b/ddnnife_bindgen/src/main.rs new file mode 100644 index 0000000..f6cff6c --- /dev/null +++ b/ddnnife_bindgen/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} diff --git a/ddnnife_dhone/Cargo.toml b/ddnnife_dhone/Cargo.toml index 49a379d..646e337 100644 --- a/ddnnife_dhone/Cargo.toml +++ b/ddnnife_dhone/Cargo.toml @@ -12,6 +12,6 @@ name = "dhone" path = "src/main.rs" [dependencies] -clap = { version = "4.5", features = ["derive"] } -ddnnife = { path = "../ddnnife" } -mimalloc = "0.1" +clap = { workspace = true } +ddnnife = { workspace = true } +mimalloc = { workspace = true } diff --git a/deny.toml b/deny.toml index b19e94b..5815282 100644 --- a/deny.toml +++ b/deny.toml @@ -1,8 +1,9 @@ [licenses] allow = [ - "LGPL-3.0", - "MIT", "BSD-3-Clause", "BSL-1.0", + "LGPL-3.0", + "MIT", + "MPL-2.0", "Unicode-DFS-2016", ] diff --git a/flake.lock b/flake.lock index 81a5373..a9a4f7a 100644 --- a/flake.lock +++ b/flake.lock @@ -7,16 +7,16 @@ ] }, "locked": { - "lastModified": 1717383740, - "narHash": "sha256-559HbY4uhNeoYvK3H6AMZAtVfmR3y8plXZ1x6ON/cWU=", + "lastModified": 1720226507, + "narHash": "sha256-yHVvNsgrpyNTXZBEokL8uyB2J6gB1wEx0KOJzoeZi1A=", "owner": "ipetkov", "repo": "crane", - "rev": "b65673fce97d277934488a451724be94cc62499a", + "rev": "0aed560c5c0a61c9385bddff471a13036203e11c", "type": "github" }, "original": { "owner": "ipetkov", - "ref": "v0.17.3", + "ref": "v0.18.0", "repo": "crane", "type": "github" } @@ -50,11 +50,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1719296889, - "narHash": "sha256-rX9GzfrzvjfqrjfyKnX+zmXTYNRZXqEUWUX2u+LBdi0=", + "lastModified": 1722839439, + "narHash": "sha256-AwQv9kstzEOYjzuC9uY8jECqFJPuV/UxPLa30L3DLqo=", "owner": "nix-community", "repo": "fenix", - "rev": "049a6ecec1da711d3d84072732e4b14f98e0edd4", + "rev": "1388e72dd8562c8b2908fd655dee0c797df9e930", "type": "github" }, "original": { @@ -65,11 +65,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1719145550, - "narHash": "sha256-K0i/coxxTEl30tgt4oALaylQfxqbotTSNb1/+g+mKMQ=", + "lastModified": 1722651103, + "narHash": "sha256-IRiJA0NVAoyaZeKZluwfb2DoTpBAj+FLI0KfybBeDU0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e4509b3a560c87a8d4cb6f9992b8915abf9e36d8", + "rev": "a633d89c6dc9a2a8aae11813a62d7c58b2c0cc51", "type": "github" }, "original": { @@ -90,11 +90,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1719233333, - "narHash": "sha256-+BgWRK3bWVIFwdn43DGRVscnu9P63Mndyhte/hgEwUA=", + "lastModified": 1722798820, + "narHash": "sha256-/Bd0VzlutcxTwSNouS/iC6BDv395NoO4XmBJaS2vQLg=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "7b11fdeb681c12002861b9804a388efde81c9647", + "rev": "c9109f23de57359df39db6fa36b5ca4c64b671e1", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index d94da03..3b2221d 100644 --- a/flake.nix +++ b/flake.nix @@ -8,7 +8,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; crane = { - url = "github:ipetkov/crane/v0.17.3"; + url = "github:ipetkov/crane/v0.18.0"; inputs.nixpkgs.follows = "nixpkgs"; }; d4 = { @@ -37,21 +37,6 @@ "x86_64-linux" ]; - # Rust target triple mappings for the target platforms. - rust = { - aarch64-darwin.default = "aarch64-apple-darwin"; - aarch64-linux = { - default = "aarch64-unknown-linux-gnu"; - static = "aarch64-unknown-linux-musl"; - }; - x86_64-darwin.default = "x86_64-apple-darwin"; - x86_64-linux = { - default = "x86_64-unknown-linux-gnu"; - static = "x86_64-unknown-linux-musl"; - }; - x86_64-windows.default = "x86_64-pc-windows-gnu"; - }; - # Determines the build system from the given package set. # Can be used for pointing to Windows cross-builds. buildSystem = @@ -61,107 +46,6 @@ # If building for Windows, appends -windows, otherwise nothing. windowsSuffix = pkgs: name: if pkgs.stdenv.hostPlatform.isWindows then "${name}-windows" else name; - # Constructs a Rust toolchain from the build to the target system. - toolchain = - system: target: - fenix.packages.${system}.combine [ - fenix.packages.${system}.stable.defaultToolchain - fenix.packages.${system}.targets.${target}.stable.rust-std - ]; - - # A ddnnife build for the specified platform. - # Build and host packages can be used for a cross-build. - # This outputs a set of the corresponding crane library, the crate build definition, - # the package and dependeny artifacts. - ddnnife = - buildPkgs: hostPkgs: withD4: - let - buildSystem = buildPkgs.stdenv.system; - hostSystem = hostPkgs.stdenv.system; - static = !withD4 && (rust.${hostSystem} ? static); - windows = hostPkgs.stdenv.hostPlatform.isWindows; - mt-kahypar = d4.packages.${buildSystem}.${windowsSuffix hostPkgs "mt-kahypar"}; - target = if static then rust.${hostSystem}.static else rust.${hostSystem}.default; - craneLib = (crane.mkLib buildPkgs).overrideToolchain (toolchain buildSystem target); - metadata = craneLib.crateNameFromCargoToml { cargoToml = ./ddnnife/Cargo.toml; }; - - crate = - { - pname = metadata.pname; - version = metadata.version; - src = ./.; - strictDeps = true; - CARGO_BUILD_TARGET = target; - - # Darwin builds fail without libiconv. - buildInputs = lib.optionals hostPkgs.stdenv.isDarwin [ hostPkgs.libiconv ]; - } - // lib.optionalAttrs static { - # A static build needs to set the C compiler for C dependencies to be compiled correctly. - TARGET_CC = - let - cc = hostPkgs.pkgsStatic.stdenv.cc; - in - "${cc}/bin/${cc.targetPrefix}cc"; - } - // lib.optionalAttrs withD4 { - cargoExtraArgs = "--features d4"; - - buildInputs = [ - hostPkgs.boost.dev - hostPkgs.gmp.dev - mt-kahypar.dev - ] ++ lib.optionals hostPkgs.stdenv.isDarwin [ hostPkgs.libiconv ]; - - nativeBuildInputs = [ - buildPkgs.m4 - buildPkgs.pkg-config - ]; - - # FIXME: Tests with d4 are currently unable to run on x86_64-darwin. - doCheck = buildSystem != "x86_64-darwin"; - } - // lib.optionalAttrs windows ( - let - # The default MinGW GCC in nix comes with mcfgthreads which seems to be unable - # to produce static Rust binaries with C dependencies. - cc = hostPkgs.buildPackages.wrapCC ( - hostPkgs.buildPackages.gcc-unwrapped.override ({ - threadsCross = { - model = "win32"; - package = null; - }; - }) - ); - in - { - TARGET_CC = "${cc}/bin/${cc.targetPrefix}cc"; - TARGET_CXX = "${cc}/bin/${cc.targetPrefix}cc"; - - depsBuildBuild = [ - cc - hostPkgs.windows.pthreads - ]; - - # Would need Wine support to run. - doCheck = false; - } - ) - // lib.optionalAttrs (windows && withD4) { - # The Windows cross-build won't find the correct include and library directories by default. - CXXFLAGS = "-I ${hostPkgs.boost.dev}/include -I ${hostPkgs.gmp.dev}/include -I ${mt-kahypar.dev}/include"; - CARGO_BUILD_RUSTFLAGS = "-L ${mt-kahypar}/lib"; - }; - - artifacts = craneLib.buildDepsOnly crate; - in - { - inherit craneLib; - inherit crate; - inherit artifacts; - package = craneLib.buildPackage (crate // { cargoArtifacts = artifacts; }); - }; - # A simple README explaining how to setup the built directories to run the binaries. documentation = pkgs: @@ -184,7 +68,7 @@ pkgs.buildEnv { name = "ddnnife"; paths = [ - self.packages.${system}.${windowsSuffix' "ddnnife-d4"} + self.packages.${system}."${windowsSuffix' "ddnnife"}-d4" d4.packages.${system}.${windowsSuffix' "dependencies"} (documentation pkgs) ]; @@ -197,15 +81,69 @@ let pkgs = nixpkgs.legacyPackages.${system}; pkgs-windows = pkgs.pkgsCross.mingwW64; + + defaultAttrs = { + buildPkgs = pkgs; + inherit fenix; + inherit crane; + component = "ddnnife_bin"; + }; + + staticAttrs.hostPkgs = pkgs.pkgsStatic; + windowsAttrs.hostPkgs = pkgs-windows; + + d4Attrs = defaultAttrs // { + d4 = true; + mt-kahypar = d4.packages.${system}.mt-kahypar; + }; + + libAttrs = { + name = "libddnnife"; + component = "ddnnife"; + library = true; + }; in { default = self.packages.${system}.ddnnife-d4; - ddnnife = (ddnnife pkgs pkgs false).package; - ddnnife-d4 = (ddnnife pkgs pkgs true).package; + ddnnife = import ./nix/ddnnife.nix defaultAttrs; + ddnnife-static = import ./nix/ddnnife.nix (defaultAttrs // staticAttrs); - ddnnife-windows = (ddnnife pkgs pkgs-windows false).package; - ddnnife-d4-windows = (ddnnife pkgs pkgs-windows true).package; + ddnnife-d4 = import ./nix/ddnnife.nix d4Attrs; + ddnnife-d4-bundled = bundled-d4 pkgs; + + ddnnife-windows = import ./nix/ddnnife.nix (defaultAttrs // windowsAttrs); + ddnnife-windows-d4 = import ./nix/ddnnife.nix ( + d4Attrs // windowsAttrs // { mt-kahypar = d4.packages.${system}.mt-kahypar-windows; } + ); + ddnnife-windows-d4-bundled = bundled-d4 pkgs-windows; + + libddnnife = import ./nix/ddnnife.nix (defaultAttrs // libAttrs); + libddnnife-d4 = import ./nix/ddnnife.nix (d4Attrs // libAttrs); + + bindgen = import ./nix/ddnnife.nix ( + defaultAttrs + // { + name = "bindgen"; + component = "ddnnife_bindgen"; + } + ); + + python = import ./nix/ddnnife.nix ( + defaultAttrs + // { + pythonLib = true; + component = "ddnnife"; + } + ); + + documentation = import ./nix/ddnnife.nix ( + defaultAttrs + // { + documentation = true; + component = "ddnnife"; + } + ); container = pkgs.dockerTools.buildLayeredImage { name = "ddnnife"; @@ -219,29 +157,26 @@ }; }; }; - - bundled-d4 = bundled-d4 pkgs; - bundled-d4-windows = bundled-d4 pkgs-windows; } ); checks = lib.genAttrs systems ( system: let - pkgs = nixpkgs.legacyPackages.${system}; - generated = ddnnife pkgs pkgs false; - generated-d4 = ddnnife pkgs pkgs true; - craneLib = generated-d4.craneLib; + defaultAttrs = { + buildPkgs = nixpkgs.legacyPackages.${system}; + inherit fenix; + inherit crane; + }; + + d4Attrs = defaultAttrs // { + d4 = true; + mt-kahypar = d4.packages.${system}.mt-kahypar; + }; in { - format = craneLib.cargoFmt generated.crate; - lint = craneLib.cargoClippy ( - generated-d4.crate - // { - cargoArtifacts = generated-d4.artifacts; - cargoClippyExtraArgs = "--all-features -- --deny warnings"; - } - ); - deny = craneLib.cargoDeny generated.crate; + format = import ./nix/ddnnife.nix (defaultAttrs // { format = true; }); + lint = import ./nix/ddnnife.nix (d4Attrs // { lint = true; }); + deny = import ./nix/ddnnife.nix (defaultAttrs // { deny = true; }); } ); }; diff --git a/nix/ddnnife.nix b/nix/ddnnife.nix new file mode 100644 index 0000000..0c229f9 --- /dev/null +++ b/nix/ddnnife.nix @@ -0,0 +1,171 @@ +{ + buildPkgs, + hostPkgs ? buildPkgs, + fenix, + crane, + mt-kahypar ? null, + component ? "", + name ? "ddnnife", + d4 ? false, + library ? pythonLib, + pythonLib ? false, + deny ? false, + documentation ? false, + format ? false, + lint ? false, +}: +let + lib = buildPkgs.lib; + + buildSystem = buildPkgs.stdenv.system; + hostSystem = hostPkgs.stdenv.system; + + rust = import ./rust.nix { inherit fenix; }; + static = hostPkgs.hostPlatform.isStatic; + target = if static then rust.map.${hostSystem}.static else rust.map.${hostSystem}.default; + craneLib = (crane.mkLib buildPkgs).overrideToolchain (rust.toolchain buildSystem target); + + metadata = craneLib.crateNameFromCargoToml { cargoToml = ../ddnnife/Cargo.toml; }; + + features = + if (d4 || library) then + lib.concatStrings ( + [ + "--features" + " " + ] + ++ [ (lib.concatStringsSep "," (lib.optionals d4 [ "d4" ] ++ lib.optionals library [ "uniffi" ])) ] + ) + else + ""; + + craneAction = + if deny then + "cargoDeny" + else if documentation then + "cargoDoc" + else if format then + "cargoFmt" + else if lint then + "cargoClippy" + else + "buildPackage"; + + crate = + { + meta = { + mainProgram = "ddnnife"; + description = "A d-DNNF reasoner."; + homepage = "https://github.com/SoftVarE-Group/d-dnnf-reasoner"; + license = lib.licenses.lgpl3Plus; + platforms = lib.platforms.unix ++ lib.platforms.windows; + }; + + # The build differs between the variants and the dep build should therefore be named differently. + pname = lib.concatStringsSep "-" ( + [ "ddnnife" ] ++ lib.optionals d4 [ "d4" ] ++ lib.optionals library [ "uniffi" ] + ); + + version = metadata.version; + + src = ./..; + strictDeps = true; + + buildInputs = + lib.optionals d4 [ + hostPkgs.boost.dev + hostPkgs.gmp.dev + mt-kahypar.dev + ] + ++ lib.optionals hostPkgs.stdenv.isDarwin [ hostPkgs.libiconv ]; + + nativeBuildInputs = + lib.optionals d4 [ + buildPkgs.m4 + buildPkgs.pkg-config + ] + ++ lib.optionals pythonLib [ buildPkgs.maturin ]; + + cargoExtraArgs = features; + + CARGO_BUILD_TARGET = target; + TARGET_CC = "${hostPkgs.stdenv.cc}/bin/${hostPkgs.stdenv.cc.targetPrefix}cc"; + } + // lib.optionalAttrs hostPkgs.stdenv.hostPlatform.isWindows ( + let + # The default MinGW GCC in nix comes with mcfgthreads which seems to be unable + # to produce static Rust binaries with C dependencies. + cc = hostPkgs.buildPackages.wrapCC ( + hostPkgs.buildPackages.gcc-unwrapped.override ({ + threadsCross = { + model = "win32"; + package = null; + }; + }) + ); + in + { + TARGET_CC = "${cc}/bin/${cc.targetPrefix}cc"; + TARGET_CXX = "${cc}/bin/${cc.targetPrefix}cc"; + + depsBuildBuild = [ + cc + hostPkgs.windows.pthreads + ]; + + CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUNNER = ( + buildPkgs.writeShellScript "wine-wrapped" '' + export WINEPREFIX=''$(mktemp -d) + export WINEDEBUG=-all + ${buildPkgs.wineWow64Packages.minimal}/bin/wine $@ + '' + ); + } + ) + // lib.optionalAttrs (d4 && hostPkgs.stdenv.system == "x86_64-darwin") { + # FIXME: Tests with d4 are currently unable to run on x86_64-darwin. + doCheck = false; + } + // lib.optionalAttrs (d4 && hostPkgs.stdenv.hostPlatform.isWindows) { + # The Windows cross-build won't find the correct include and library directories by default. + CXXFLAGS = "-I ${hostPkgs.boost.dev}/include -I ${hostPkgs.gmp.dev}/include -I ${mt-kahypar.dev}/include"; + CARGO_BUILD_RUSTFLAGS = "-L ${mt-kahypar}/lib"; + + # FIXME: Tests with d4 are currently unable to run on x86_64-windows. + doCheck = false; + }; + + cargoArtifacts = craneLib.buildDepsOnly crate; +in +craneLib.${craneAction} ( + crate + // { + pname = name; + + cargoExtraArgs = lib.concatStringsSep " " ( + lib.optionals (component != "") [ "--package ${component}" ] ++ [ features ] + ); + + cargoTestExtraArgs = "--workspace"; + + inherit cargoArtifacts; + } + // lib.optionalAttrs (component == "ddnnife_bin" && !hostPkgs.stdenv.hostPlatform.isWindows) { + postInstall = "mv $out/bin/ddnnife_bin $out/bin/ddnnife"; + } + // lib.optionalAttrs (component == "ddnnife_bin" && hostPkgs.stdenv.hostPlatform.isWindows) { + postInstall = "mv $out/bin/ddnnife_bin.exe $out/bin/ddnnife.exe"; + } + // lib.optionalAttrs pythonLib { + buildPhaseCargoCommand = '' + cd bindings/python + maturin build --offline + ''; + + installPhaseCommand = '' + mkdir -p $out + cp ../../target/wheels/* $out/ + ''; + } + // lib.optionalAttrs lint { cargoClippyExtraArgs = "--all-features -- --deny warnings"; } +) diff --git a/nix/rust.nix b/nix/rust.nix new file mode 100644 index 0000000..f83aaea --- /dev/null +++ b/nix/rust.nix @@ -0,0 +1,25 @@ +{ fenix }: +{ + # Rust target triple mappings for the target platforms. + map = { + aarch64-darwin.default = "aarch64-apple-darwin"; + aarch64-linux = { + default = "aarch64-unknown-linux-gnu"; + static = "aarch64-unknown-linux-musl"; + }; + x86_64-darwin.default = "x86_64-apple-darwin"; + x86_64-linux = { + default = "x86_64-unknown-linux-gnu"; + static = "x86_64-unknown-linux-musl"; + }; + x86_64-windows.default = "x86_64-pc-windows-gnu"; + }; + + # Constructs a Rust toolchain for the build system with a (possibly different) target system. + toolchain = + system: target: + fenix.packages.${system}.combine [ + fenix.packages.${system}.stable.defaultToolchain + fenix.packages.${system}.targets.${target}.stable.rust-std + ]; +}