From 4a7e990fbde0df78284b51ffb771ef77407495a5 Mon Sep 17 00:00:00 2001 From: Pieter Date: Fri, 9 Jun 2023 18:23:46 +0200 Subject: [PATCH] feat: Initial code (#1) * misc: create nbuild-core * feat: build for simple package * feat: simple binary * feat: workspace support * feat: deep dependencies * feat: reusing crates * refactor: multiple versions of a crate * feat: resolve dependencies * feat: edition * feat: build dependencies * feat: handle custom lib paths * feat: handle custom build paths * feat: handle proc-macros * feat: handle targets | multiple dependencies * feat: tracing info * bug: dev dependencies appearing in normal dependencies * feat: features activating features on optional dependencies * refactor: more tracing * bug: features on default dependencies * refactor: log all metadata * refactor: reduce loop in unpacking chain * feat: handle dependency renames * bug: optional build packages * refactor: clean local sources * feat: download from crates.io * feat: custom rust version * feat: missing crate overrides * refactor: better fetchCrate override * refactor: faster builds by matching cargo more * refactor: call nix build directly * refactor: sort features for reproducible builds * refactor: flake safe overlay * tests: deterministic features * refactor: split into models * refactor: combine visitors * refactor: comments * refactor: better error handling * refactor: clippy suggestions * ci: add rust test * misc: READMEs * tests: work on ci * misc: publish metadata * feat: handle lib renames * feat: match dependencies on version * refactor: don't clean defaults * refactor: verbose renames * misc: nix command instructions * misc: v0.1.1 --- .github/workflows/rust.yml | 22 + .gitignore | 3 + Cargo.lock | 764 +++++++++++ Cargo.toml | 13 + README.md | 34 + nbuild-core/Cargo.toml | 19 + nbuild-core/README.md | 5 + nbuild-core/src/lib.rs | 18 + nbuild-core/src/models/cargo/mod.rs | 748 +++++++++++ nbuild-core/src/models/cargo/visitor.rs | 1158 +++++++++++++++++ nbuild-core/src/models/mod.rs | 623 +++++++++ nbuild-core/src/models/nix.rs | 834 ++++++++++++ nbuild-core/tests/simple/Cargo.lock | 23 + nbuild-core/tests/simple/Cargo.toml | 12 + nbuild-core/tests/simple/src/main.rs | 7 + nbuild-core/tests/workspace/Cargo.lock | 62 + nbuild-core/tests/workspace/Cargo.toml | 8 + nbuild-core/tests/workspace/child/Cargo.toml | 16 + nbuild-core/tests/workspace/child/src/lib.rs | 34 + nbuild-core/tests/workspace/parent/Cargo.toml | 25 + .../tests/workspace/parent/src/main.rs | 6 + nbuild-core/tests/workspace/rename/Cargo.toml | 9 + nbuild-core/tests/workspace/rename/src/lib.rs | 14 + .../tests/workspace/targets/Cargo.toml | 8 + .../tests/workspace/targets/src/lib.rs | 9 + nbuild/Cargo.toml | 16 + nbuild/src/main.rs | 59 + 27 files changed, 4549 insertions(+) create mode 100644 .github/workflows/rust.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 nbuild-core/Cargo.toml create mode 100644 nbuild-core/README.md create mode 100644 nbuild-core/src/lib.rs create mode 100644 nbuild-core/src/models/cargo/mod.rs create mode 100644 nbuild-core/src/models/cargo/visitor.rs create mode 100644 nbuild-core/src/models/mod.rs create mode 100644 nbuild-core/src/models/nix.rs create mode 100644 nbuild-core/tests/simple/Cargo.lock create mode 100644 nbuild-core/tests/simple/Cargo.toml create mode 100644 nbuild-core/tests/simple/src/main.rs create mode 100644 nbuild-core/tests/workspace/Cargo.lock create mode 100644 nbuild-core/tests/workspace/Cargo.toml create mode 100644 nbuild-core/tests/workspace/child/Cargo.toml create mode 100644 nbuild-core/tests/workspace/child/src/lib.rs create mode 100644 nbuild-core/tests/workspace/parent/Cargo.toml create mode 100644 nbuild-core/tests/workspace/parent/src/main.rs create mode 100644 nbuild-core/tests/workspace/rename/Cargo.toml create mode 100644 nbuild-core/tests/workspace/rename/src/lib.rs create mode 100644 nbuild-core/tests/workspace/targets/Cargo.toml create mode 100644 nbuild-core/tests/workspace/targets/src/lib.rs create mode 100644 nbuild/Cargo.toml create mode 100644 nbuild/src/main.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..f783b2e --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Clippy + run: cargo clippy --all-targets --no-deps + - name: Run tests + run: cargo test --all diff --git a/.gitignore b/.gitignore index 1b7553c..d15f76d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ tmp/ .direnv/ +target/ + +**/result diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8843edb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,764 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "camino" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-lock" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11c675378efb449ed3ce8de78d75d0d80542fc98487c26aba28eb3b82feac72" +dependencies = [ + "semver", + "serde", + "toml", + "url", +] + +[[package]] +name = "cargo-nbuild" +version = "0.1.1" +dependencies = [ + "nbuild-core", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "cargo-platform" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +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 = "cfg-expr" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8790cf1286da485c72cf5fc7aeba308438800036ec67d89425924c4807268c9" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "guppy-workspace-hack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92620684d99f750bae383ecb3be3748142d6095760afd5cbcf2261e9a279d780" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "nbuild-core" +version = "0.1.1" +dependencies = [ + "cargo-lock", + "cargo_metadata", + "pretty_assertions", + "target-spec", + "thiserror", + "tracing", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pretty_assertions" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" +dependencies = [ + "ctor", + "diff", + "output_vt100", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +dependencies = [ + "regex-syntax 0.7.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" + +[[package]] +name = "target-spec" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf4306559bd50cb358e7af5692694d6f6fad95cf2c0bea2571dd419f5298e12" +dependencies = [ + "cfg-expr", + "guppy-workspace-hack", + "target-lexicon", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "toml" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +dependencies = [ + "memchr", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0db4142 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +members = [ + "nbuild", + "nbuild-core", +] + +[workspace.package] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/shuttle-hq/cargo-nbuild" + +[workspace.dependencies] +tracing = "0.1.37" diff --git a/README.md b/README.md new file mode 100644 index 0000000..55f8083 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +A cargo builder that uses the [`buildRustCrate`][buildRustCrate] from the nix package manager. + +This yields the following benefits: +- **Sandbox builds**: A malicious dependency in project `A` cannot alter the filesystem or inject source code into libraries that will affect the build of another project `B`. +- **Shared cache**: If project `A` has a dependency on some crate, let's say `tokio`, with features `macros` and `rt`, then this builder will cache each dependency individually. So if project `B` also uses `tokio` with the same features and version, then the `tokio` dependency will not be rebuild. +- **Reproducible**: Given the same version and targets, any project will build exactly the same on different machines. + +## Install + +``` shell +cargo install cargo-nbuild +``` + +> :warning: The nix package manager needs to be [installed](https://nixos.org/download.html) on your system. + +> :bulb: You also need to enable the new [nix command](https://nixos.wiki/wiki/Nix_command) in the user specific configuration or system wide configuration. + +## Usage +From a Rust project run + +``` shell +cargo nbuild +``` + +## Missing +This builder is still in early days and is missing features + +- Choosing target: like with `cargo build --target ...` +- Choosing workspace package: builds only work when inside the workspace member, and not when you are at the workspace root. Ie the `cargo build --package ...` equavalent is missing. +- Remote builds: nix supports remote builds which are not currently possible +- Custom rust version: it should be possible to change the version of rustc used for the compiles +- ... other `cargo build` options + +[buildRustCrate]: https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/rust.section.md#buildrustcrate-compiling-rust-crates-using-nix-instead-of-cargo-compiling-rust-crates-using-nix-instead-of-cargo diff --git a/nbuild-core/Cargo.toml b/nbuild-core/Cargo.toml new file mode 100644 index 0000000..9d01253 --- /dev/null +++ b/nbuild-core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nbuild-core" +version = "0.1.1" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Core library for cargo-nbuild" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cargo-lock = "9.0.0" +cargo_metadata = "0.15.4" +target-spec = "1.4.0" +thiserror = "1.0.40" +tracing = { workspace = true } + +[dev-dependencies] +pretty_assertions = "1.3.0" diff --git a/nbuild-core/README.md b/nbuild-core/README.md new file mode 100644 index 0000000..4fc631b --- /dev/null +++ b/nbuild-core/README.md @@ -0,0 +1,5 @@ +This crate is used to create a nix derivation file. The derivation uses [`buildRustCrate`][buildRustCrate] to build +and cache each dependency individually. This allows the cache to be shared between projects if the dependency is +the same version with the same features activated. + +[buildRustCrate]: https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/rust.section.md#buildrustcrate-compiling-rust-crates-using-nix-instead-of-cargo-compiling-rust-crates-using-nix-instead-of-cargo diff --git a/nbuild-core/src/lib.rs b/nbuild-core/src/lib.rs new file mode 100644 index 0000000..49f6969 --- /dev/null +++ b/nbuild-core/src/lib.rs @@ -0,0 +1,18 @@ +#![doc = include_str!("../README.md")] + +use thiserror::Error; + +pub mod models; + +/// Errors that can happen while reading cargo metadata +#[derive(Debug, Error)] +pub enum Error { + #[error("target spec failed: {0}")] + TargetSpec(#[from] target_spec::Error), + + #[error("failed to read cargo metadata: {0}")] + Metadata(#[from] cargo_metadata::Error), + + #[error("failed to read cargo lock file: {0}")] + LockFile(#[from] cargo_lock::Error), +} diff --git a/nbuild-core/src/models/cargo/mod.rs b/nbuild-core/src/models/cargo/mod.rs new file mode 100644 index 0000000..e5b3940 --- /dev/null +++ b/nbuild-core/src/models/cargo/mod.rs @@ -0,0 +1,748 @@ +//! Models used to read in cargo metadata. It is also used to determine which optional dependencies to enable, and +//! which features to enable. + +use std::{ + cell::RefCell, + collections::{BTreeMap, HashMap, HashSet}, + path::PathBuf, + rc::Rc, +}; + +use cargo_lock::{Lockfile, Version}; +use cargo_metadata::{camino::Utf8PathBuf, DependencyKind, MetadataCommand, PackageId}; +use target_spec::{Platform, TargetSpec}; +use tracing::{instrument, trace}; + +use crate::Error; + +use super::Source; + +mod visitor; + +pub use visitor::Visitor; + +/// Details of a package / crate +#[derive(Debug, PartialEq, Clone)] +pub struct Package { + pub(super) name: String, + pub(super) version: Version, + pub(super) source: Source, + pub(super) lib_name: Option, + pub(super) lib_path: Option, + pub(super) build_path: Option, + pub(super) proc_macro: bool, + + /// List of possible features for a package + pub(super) features: HashMap>, + + /// List of features that has been enabled + pub(super) enabled_features: HashSet, + pub(super) dependencies: Vec, + pub(super) build_dependencies: Vec, + pub(super) edition: String, +} + +/// A dependency of a package. This model is used to keep track of [renames][rename], [optional][optional] dependencies, +/// [enabled features][features], and whether [`default-features`][default] is active or not. +/// +/// [rename]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#renaming-dependencies-in-cargotoml +/// [optional]: https://doc.rust-lang.org/cargo/reference/features.html#optional-dependencies +/// [features]: https://doc.rust-lang.org/cargo/reference/features.html#dependency-features +/// [default]: https://doc.rust-lang.org/cargo/reference/features.html#dependency-features +#[derive(Debug, PartialEq, Clone)] +pub struct Dependency { + pub(super) name: String, + pub(super) package: Rc>, + pub(super) optional: bool, + pub(super) uses_default_features: bool, + pub(super) features: Vec, +} + +impl Package { + /// Get a package from a path with a `Cargo.toml` file + pub fn from_current_dir(path: impl Into) -> Result { + let platform = Platform::current()?; + + let metadata = MetadataCommand::new() + .current_dir(path) + .other_options(vec![ + "--filter-platform".to_string(), + platform.triple_str().to_string(), + ]) + .exec()?; + let lock_file = metadata.workspace_root.join("Cargo.lock"); + let lock_file = Lockfile::load(lock_file)?; + + trace!(?platform, ?metadata, ?lock_file, "have metadata"); + + let packages = + BTreeMap::from_iter(metadata.packages.iter().map(|p| (p.id.clone(), p.clone()))); + let nodes = BTreeMap::from_iter( + metadata + .resolve + .as_ref() + .expect("metadata to have a resolve section") + .nodes + .iter() + .map(|n| (n.id.clone(), n.clone())), + ); + let checksums = BTreeMap::from_iter(lock_file.packages.iter().filter_map(|p| { + p.checksum.as_ref().map(|checksum| { + ( + (p.name.to_string(), p.version.to_string()), + checksum.to_string(), + ) + }) + })); + + let root_id = metadata + .resolve + .as_ref() + .expect("metadata to have a resolve section") + .root + .as_ref() + .expect("a root from metadata") + .clone(); + + let mut resolved_packages = Default::default(); + + Ok(Self::get_package( + root_id, + &packages, + &nodes, + &checksums, + &mut resolved_packages, + &platform, + )) + } + + /// Recursively get a package and its dependencies. Use the `resolved_packages` to make sure we only + /// have one reverence to re-occuring packages. + fn get_package( + id: PackageId, + packages: &BTreeMap, + nodes: &BTreeMap, + checksums: &BTreeMap<(String, String), String>, + resolved_packages: &mut BTreeMap>>, + platform: &Platform, + ) -> Self { + let node = nodes.get(&id).expect("node to exist").clone(); + let package = packages.get(&id).expect("package to exist"); + + trace!( + package.name, + ?package.features, + ?node, + "found package and node" + ); + + let features = package.features.clone(); + let package_dependencies: Vec<_> = package + .dependencies + .iter() + .filter(|d| d.kind == DependencyKind::Normal) + .cloned() + .collect(); + let package_build_dependencies: Vec<_> = package + .dependencies + .iter() + .filter(|d| d.kind == DependencyKind::Build) + .cloned() + .collect(); + + let dependencies = node + .dependencies + .iter() + .filter_map(|id| { + Dependency::get_dependency( + id, + &package_dependencies, + packages, + nodes, + checksums, + resolved_packages, + platform, + ) + }) + .collect(); + let build_dependencies = node + .dependencies + .iter() + .filter_map(|id| { + Dependency::get_dependency( + id, + &package_build_dependencies, + packages, + nodes, + checksums, + resolved_packages, + platform, + ) + }) + .collect(); + + // Safe to unwrap since the manifest has to be in some directory + let package_path: PathBuf = package.manifest_path.parent().unwrap().into(); + + let (lib_path, lib_name) = package + .targets + .iter() + .find(|t| { + t.kind.iter().any(|k| { + matches!( + k.as_str(), + "lib" | "cdylib" | "dylib" | "rlib" | "proc-macro" + ) + }) + }) + .map(|t| { + ( + t.src_path + .strip_prefix(&package_path) + .unwrap() // Safe to unwrap since the src has to be in the package path + .to_path_buf(), + t.name.clone(), + ) + }) + .unzip(); + let build_path = package + .targets + .iter() + .find(|t| t.kind.iter().any(|k| k == "custom-build")) + .map(|t| { + t.src_path + .strip_prefix(&package_path) + .unwrap() // Safe to unwrap since the src has to be in the package path + .to_path_buf() + }); + let proc_macro = package + .targets + .iter() + .any(|t| t.kind.iter().any(|k| k == "proc-macro")); + + let source = if package.source.is_some() { + let checksum = checksums + .get(&(package.name.to_string(), package.version.to_string())) + .expect("to have a checksum"); + Source::CratesIo(checksum.to_string()) + } else { + Source::Local(package_path) + }; + + Self { + name: package.name.clone(), + version: package.version.clone(), + source, + lib_name, + lib_path, + build_path, + proc_macro, + dependencies, + build_dependencies, + features, + enabled_features: Default::default(), + edition: package.edition.to_string(), + } + } + + /// Resolve all the optional dependencies and enabled features of a package. This is done recursively and only + /// needed on the top level package. + pub fn resolve(&mut self) { + self.visit(&mut visitor::ResolveVisitor); + } + + /// Helper to call visitor easier. + fn visit(&mut self, visitor: &mut impl visitor::Visitor) { + visitor.visit(self); + } + + /// Get an iter for all the dependencies of a package. This is both normal dependencies and build dependencies. + pub fn dependencies_iter(&self) -> impl Iterator { + self.dependencies + .iter() + .chain(self.build_dependencies.iter()) + } + + /// Get a mutable iter for all the dependencies of a package. This is both normal dependencies and build dependencies. + pub fn dependencies_iter_mut(&mut self) -> impl Iterator { + self.dependencies + .iter_mut() + .chain(self.build_dependencies.iter_mut()) + } +} + +impl Dependency { + /// Recursively get a dependency and its package. Use the `resolved_packages` to make sure we only + /// have one reverence to re-occuring packages - this is needed during feature resolution + #[instrument(skip_all, fields(%id))] + fn get_dependency( + id: &PackageId, + parent_dependencies: &[cargo_metadata::Dependency], + packages: &BTreeMap, + nodes: &BTreeMap, + checksums: &BTreeMap<(String, String), String>, + resolved_packages: &mut BTreeMap>>, + platform: &Platform, + ) -> Option { + let package = match resolved_packages.get(id) { + Some(package) => Rc::clone(package), + None => { + let package = RefCell::new(Package::get_package( + id.clone(), + packages, + nodes, + checksums, + resolved_packages, + platform, + )) + .into(); + + resolved_packages.insert(id.clone(), Rc::clone(&package)); + + package + } + }; + + // Handle renames + let name = package.borrow().name.clone(); + let version = package.borrow().version.clone(); + + // A dependency may appear more than once because of targets. So only get those that match the current target. + // + // https://doc.rust-lang.org/cargo/reference/config.html#target + let dependencies: Vec<_> = parent_dependencies + .iter() + .filter(|d| d.name == name) + .filter(|d| d.req.matches(&version)) + .filter(|d| match &d.target { + Some(target_spec) => { + // Safe to unwrap since cargo would have failed if the target spec was not valid + let target_spec = TargetSpec::new(target_spec.to_string()).unwrap(); + + target_spec.eval(platform).unwrap_or(false) + } + None => true, + }) + .collect(); + + // It could happen that this kind of dependency is not part of the kind passed into this function, + // in which case this dependency should not we considered as a real dependency. + if dependencies.is_empty() { + return None; + } + + // Start with sane default assumptions + let mut optional = true; + let mut uses_default_features = false; + let mut features: Vec = Default::default(); + let mut dependency_name: String = Default::default(); + let mut dependency_rename = None; + + for dependency in dependencies { + if !dependency.optional { + optional = false; + } + + if dependency.uses_default_features { + uses_default_features = true; + } + + // Features should be additive + features.extend(dependency.features.iter().cloned()); + + if dependency_rename.is_none() && dependency.rename.is_some() { + dependency_rename = dependency.rename.clone(); + } + + if dependency_name.is_empty() { + dependency_name = dependency.name.clone(); + } + } + + if let Some(dependency_rename) = dependency_rename { + dependency_name = dependency_rename; + }; + + trace!( + name, + dependency_name, + optional, + uses_default_features, + ?features, + "done with dependency" + ); + + Some(Self { + name: dependency_name, + package, + optional, + uses_default_features, + features, + }) + } +} + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, collections::HashMap, path::PathBuf, str::FromStr}; + + use crate::models::cargo::{Dependency, Package}; + + use pretty_assertions::assert_eq; + + #[test] + fn simple_package() { + let path = PathBuf::from_str(env!("CARGO_MANIFEST_DIR")) + .unwrap() + .join("tests") + .join("simple"); + + let package = Package::from_current_dir(path.clone()).unwrap(); + + assert_eq!( + package, + Package { + name: "simple".to_string(), + source: path.into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + version: "0.1.0".parse().unwrap(), + dependencies: vec![Dependency { + name: "itoa".to_string(), + package: RefCell::new(Package { + name: "itoa".to_string(), + version: "1.0.6".parse().unwrap(), + source: "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + .into(), + lib_name: Some("itoa".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([( + "no-panic".to_string(), + vec!["dep:no-panic".to_string()] + )]), + enabled_features: Default::default(), + edition: "2018".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + },], + build_dependencies: vec![Dependency { + name: "arbitrary".to_string(), + package: RefCell::new(Package { + name: "arbitrary".to_string(), + version: "1.3.0".parse().unwrap(), + source: "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" + .into(), + lib_name: Some("arbitrary".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("derive".to_string(), vec!["derive_arbitrary".to_string()]), + ( + "derive_arbitrary".to_string(), + vec!["dep:derive_arbitrary".to_string()] + ), + ]), + enabled_features: Default::default(), + edition: "2018".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + },], + features: Default::default(), + enabled_features: Default::default(), + edition: "2021".to_string(), + } + ); + } + + #[test] + fn workspace() { + let workspace = PathBuf::from_str(env!("CARGO_MANIFEST_DIR")) + .unwrap() + .join("tests") + .join("workspace"); + let path = workspace.join("parent"); + + let package = Package::from_current_dir(path.clone()).unwrap(); + + assert_eq!( + package, + Package { + name: "parent".to_string(), + version: "0.1.0".parse().unwrap(), + source: path.into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: vec![ + Dependency { + name: "child".to_string(), + package: RefCell::new(Package { + name: "child".to_string(), + version: "0.1.0".parse().unwrap(), + source: workspace.join("child").into(), + lib_name: Some("child".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: vec![ + Dependency { + name: "fnv".to_string(), + package: RefCell::new(Package { + name: "fnv".to_string(), + version: "1.0.7".parse().unwrap(), + source: "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1".into(), + lib_name: Some("fnv".to_string()), + lib_path: Some("lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("default".to_string(), vec!["std".to_string()]), + ("std".to_string(), vec![]), + ]), + enabled_features: Default::default(), + edition: "2015".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + Dependency { + name: "itoa".to_string(), + package: RefCell::new(Package { + name: "itoa".to_string(), + version: "1.0.6".parse().unwrap(), + source: "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6".into(), + lib_name: Some("itoa".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([( + "no-panic".to_string(), + vec!["dep:no-panic".to_string()] + )]), + enabled_features: Default::default(), + edition: "2018".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + Dependency { + name: "libc".to_string(), + package: RefCell::new(Package { + name: "libc".to_string(), + version: "0.2.144".parse().unwrap(), + source: "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1".into(), + lib_name: Some("libc".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: Some("build.rs".into()), + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("std".to_string(), vec![]), + ("default".to_string(), vec!["std".to_string()]), + ("use_std".to_string(), vec!["std".to_string()]), + ("extra_traits".to_string(), vec![]), + ("align".to_string(), vec![]), + ( + "rustc-dep-of-std".to_string(), + vec![ + "align".to_string(), + "rustc-std-workspace-core".to_string() + ] + ), + ("const-extern-fn".to_string(), vec![]), + ( + "rustc-std-workspace-core".to_string(), + vec!["dep:rustc-std-workspace-core".to_string()] + ), + ]), + enabled_features: Default::default(), + edition: "2015".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + Dependency { + name: "new_name".to_string(), + package: RefCell::new(Package { + name: "rename".to_string(), + version: "0.1.0".parse().unwrap(), + source: workspace.join("rename").into(), + lib_name: Some("lib_rename".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + enabled_features: Default::default(), + edition: "2021".to_string(), + }) + .into(), + optional: true, + uses_default_features: true, + features: Default::default(), + }, + Dependency { + name: "rustversion".to_string(), + package: RefCell::new(Package { + name: "rustversion".to_string(), + version: "1.0.12".parse().unwrap(), + source: "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06".into(), + lib_name: Some("rustversion".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: Some("build/build.rs".into()), + proc_macro: true, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + enabled_features: Default::default(), + edition: "2018".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + ], + build_dependencies: Default::default(), + features: HashMap::from([ + ( + "default".to_string(), + vec!["one".to_string(), "two".to_string()] + ), + ("one".to_string(), vec!["new_name".to_string()]), + ("two".to_string(), vec![]), + ("new_name".to_string(), vec!["dep:new_name".to_string()]), + ]), + enabled_features: Default::default(), + edition: "2021".to_string(), + }) + .into(), + optional: false, + uses_default_features: false, + features: vec!["one".to_string()], + }, + Dependency { + name: "itoa".to_string(), + package: RefCell::new(Package { + name: "itoa".to_string(), + version: "0.4.8".parse().unwrap(), + source: "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4".into(), + lib_name: Some("itoa".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("default".to_string(), vec!["std".to_string()]), + ("std".to_string(), vec![]), + ("i128".to_string(), vec![]), + ]), + enabled_features: Default::default(), + edition: "2015".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + Dependency { + name: "libc".to_string(), + package: RefCell::new(Package { + name: "libc".to_string(), + version: "0.2.144".parse().unwrap(), + source: "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1".into(), + lib_name: Some("libc".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: Some("build.rs".into()), + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("std".to_string(), vec![]), + ("default".to_string(), vec!["std".to_string()]), + ("use_std".to_string(), vec!["std".to_string()]), + ("extra_traits".to_string(), vec![]), + ("align".to_string(), vec![]), + ( + "rustc-dep-of-std".to_string(), + vec![ + "align".to_string(), + "rustc-std-workspace-core".to_string() + ] + ), + ("const-extern-fn".to_string(), vec![]), + ( + "rustc-std-workspace-core".to_string(), + vec!["dep:rustc-std-workspace-core".to_string()] + ), + ]), + enabled_features: Default::default(), + edition: "2015".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + Dependency { + name: "targets".to_string(), + package: RefCell::new(Package { + name: "targets".to_string(), + version: "0.1.0".parse().unwrap(), + source: workspace.join("targets").into(), + lib_name: Some("targets".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("unix".to_string(), vec![]), + ("windows".to_string(), vec![]), + ]), + enabled_features: Default::default(), + edition: "2021".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: vec!["unix".to_string()], + }, + ], + build_dependencies: Default::default(), + features: Default::default(), + enabled_features: Default::default(), + edition: "2021".to_string(), + } + ); + } +} diff --git a/nbuild-core/src/models/cargo/visitor.rs b/nbuild-core/src/models/cargo/visitor.rs new file mode 100644 index 0000000..be4f503 --- /dev/null +++ b/nbuild-core/src/models/cargo/visitor.rs @@ -0,0 +1,1158 @@ +use tracing::{info_span, trace}; + +use super::{Dependency, Package}; + +/// A visitor over cargo packages +pub trait Visitor { + /// Entry point for a visitor. Defaults to visiting all dependencies which are not optional. + fn visit(&mut self, package: &mut Package) + where + Self: Sized, + { + self.visit_package(package); + + for dependency in package.dependencies_iter() { + let dependency_span = info_span!( + "processing dependency", + name = dependency.name, + package_name = dependency.package.borrow().name, + optional = dependency.optional, + ); + let _dependency_span_guard = dependency_span.enter(); + + if !dependency.optional { + self.visit_dependency(dependency); + + dependency.package.borrow_mut().visit(self); + } + } + } + + /// Visit a package + fn visit_package(&mut self, _package: &mut Package) {} + + /// Visit a dependency of a package + fn visit_dependency(&mut self, _dependency: &Dependency) {} +} + +/// Visitor to resolve the enabled dependencies and the features on those dependencies +pub struct ResolveVisitor; + +impl Visitor for ResolveVisitor { + fn visit_dependency(&mut self, dependency: &Dependency) { + add_default(dependency); + activate_features(dependency); + } + + fn visit_package(&mut self, package: &mut Package) { + loop { + let new_features = unpack_features(package); + + if !new_features.is_empty() { + trace!(?new_features, "adding new features"); + + package.enabled_features.extend(new_features); + } else { + break; + } + } + + unpack_optionals_features(package); + } +} + +/// Add the "default" feature if default-features is not false +/// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choosing-features +fn add_default(dependency: &Dependency) { + if dependency.uses_default_features + && dependency.package.borrow().features.contains_key("default") + { + trace!("enabling default feature"); + + dependency + .package + .borrow_mut() + .enabled_features + .insert("default".to_string()); + } +} + +/// Activate all the feature on a dependency +fn activate_features(dependency: &Dependency) { + if !dependency.features.is_empty() { + let features: Vec = dependency + .features + .clone() + .iter() + .filter(|&f| dependency.package.borrow().features.contains_key(f)) + .cloned() + .collect(); + + trace!(?features, "enabling features"); + + dependency + .package + .borrow_mut() + .enabled_features + .extend(features); + } +} + +/// Get new features on a crate's "chain" that have not been seen before +fn unpack_features(package: &mut Package) -> Vec { + package + .enabled_features + .iter() + .filter_map(|f| package.features.get(f)) + .flatten() + .cloned() + .filter(|f| !package.enabled_features.contains(f)) // Don't process a "leaf" feature + .filter_map(|f| { + // Activate an optional dependency that is turned on by a feature + // https://doc.rust-lang.org/cargo/reference/features.html#optional-dependencies + if let Some(dependency_name) = f.strip_prefix("dep:") { + if let Some(dependency) = package + .dependencies + .iter_mut() + .chain(package.build_dependencies.iter_mut()) + .find(|d| d.name == dependency_name) + { + trace!(name = dependency_name, "activating optional dependency"); + dependency.optional = false; + } + + // We are activating an optional dependency and not enabling a new feature + return None; + } else { + // Activate a dependency's features + // https://doc.rust-lang.org/cargo/reference/features.html#dependency-features + if let Some((dependency_name, feature)) = f.split_once('/') { + if let Some(dependency) = package + .dependencies + .iter_mut() + .chain(package.build_dependencies.iter_mut()) + .find(|d| d.name == dependency_name) + { + let feature = feature.to_string(); + + if !dependency.features.contains(&feature) { + dependency.features.push(feature); + } + + return Some(dependency_name.to_string()); + } + } + } + + Some(f) + }) + .filter(|f| !package.enabled_features.contains(f)) // We only want to unpack new features + .collect() +} + +/// Activate features on optional dependencies where the dependencies was made non-optional by a previous feature +/// https://doc.rust-lang.org/cargo/reference/features.html#dependency-features +fn unpack_optionals_features(package: &mut Package) { + let new_dependencies_features: Vec<_> = package + .enabled_features + .iter() + .filter_map(|f| f.split_once("?/")) + .map(|(d, f)| (d.to_string(), f.to_string())) + .collect(); + + for (dependency_name, feature) in new_dependencies_features { + if let Some(dependency) = package + .dependencies_iter_mut() + .find(|d| d.name == dependency_name && !d.optional) + { + if !dependency.features.contains(&feature) { + dependency.features.push(feature.clone()); + } + + trace!( + dependency = dependency_name, + feature, + "adding feature on optional dependency" + ); + } + + package + .enabled_features + .remove(&format!("{dependency_name}?/{feature}")); + } +} + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, collections::HashMap, rc::Rc}; + + use crate::models::cargo::{Dependency, Package}; + + use pretty_assertions::assert_eq; + + fn make_package_node( + name: &str, + features: Vec<(&str, Vec<&str>)>, + dependency: Option, + ) -> Package { + let dependencies = if let Some(dependency) = dependency { + vec![dependency] + } else { + Default::default() + }; + + Package { + name: name.to_string(), + lib_name: None, + version: "0.1.0".parse().unwrap(), + source: "sha".into(), + lib_path: None, + build_path: None, + proc_macro: false, + dependencies, + build_dependencies: Default::default(), + features: HashMap::from_iter(features.into_iter().map(|(b, d)| { + ( + b.to_string(), + d.into_iter().map(ToString::to_string).collect(), + ) + })), + enabled_features: Default::default(), + edition: "2021".to_string(), + } + } + + // Defaults should not be enabled when no-defaults is used + #[test] + fn no_defaults() { + let mut child = make_package_node( + "child", + vec![ + ("default", vec!["one", "two"]), + ("one", vec![]), + ("two", vec![]), + ], + None, + ); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: false, + features: vec!["one".to_string()], + }), + ); + + input.resolve(); + + child.enabled_features.insert("one".to_string()); + let expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: false, + features: vec!["one".to_string()], + }), + ); + + assert_eq!(input, expected); + } + + // Enable defaults correctly + #[test] + fn defaults() { + let mut child = make_package_node( + "child", + vec![ + ("default", vec!["one", "two"]), + ("one", vec![]), + ("two", vec![]), + ], + None, + ); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec![], + }), + ); + + input.resolve(); + + child.enabled_features.extend([ + "one".to_string(), + "two".to_string(), + "default".to_string(), + ]); + let expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec![], + }), + ); + + assert_eq!(input, expected); + } + + // Enable everything on a chain of defaults + #[test] + fn defaults_chain() { + let mut child = make_package_node( + "child", + vec![ + ("default", vec!["one"]), + ("one", vec!["two"]), + ("two", vec![]), + ], + None, + ); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec![], + }), + ); + + input.resolve(); + + child.enabled_features.extend([ + "one".to_string(), + "two".to_string(), + "default".to_string(), + ]); + let expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec![], + }), + ); + + assert_eq!(input, expected); + } + + // Optionals should not enable default features since they will not be used + #[test] + fn optional_no_defaults() { + let child = make_package_node( + "child", + vec![ + ("default", vec!["one", "two"]), + ("one", vec![]), + ("two", vec![]), + ], + None, + ); + let build = make_package_node("build", vec![("default", vec!["hi"]), ("hi", vec![])], None); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: true, + uses_default_features: true, + features: vec![], + }), + ); + input.build_dependencies.push(Dependency { + name: "build".to_string(), + package: RefCell::new(build.clone()).into(), + optional: true, + uses_default_features: true, + features: vec![], + }); + + input.resolve(); + + let mut expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child).into(), + optional: true, + uses_default_features: true, + features: vec![], + }), + ); + expected.build_dependencies.push(Dependency { + name: "build".to_string(), + package: RefCell::new(build).into(), + optional: true, + uses_default_features: true, + features: vec![], + }); + + assert_eq!(input, expected); + } + + // Optionals should not enable any features since they will not be used + #[test] + fn optional_features() { + let child = make_package_node("child", vec![("one", vec![]), ("two", vec![])], None); + let build = make_package_node("build", vec![("hi", vec![])], None); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: true, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + input.build_dependencies.push(Dependency { + name: "build".to_string(), + package: RefCell::new(build.clone()).into(), + optional: true, + uses_default_features: true, + features: vec!["hi".to_string()], + }); + + input.resolve(); + + let mut expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child).into(), + optional: true, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + expected.build_dependencies.push(Dependency { + name: "build".to_string(), + package: RefCell::new(build).into(), + optional: true, + uses_default_features: true, + features: vec!["hi".to_string()], + }); + + assert_eq!(input, expected); + } + + // Enable everything on a chain + #[test] + fn chain() { + let mut child = make_package_node( + "child", + vec![ + ("one", vec!["two"]), + ("two", vec!["three"]), + ("three", vec![]), + ], + None, + ); + let mut build = make_package_node( + "build", + vec![("hi", vec!["world"]), ("world", vec![])], + None, + ); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + input.build_dependencies.push(Dependency { + name: "build".to_string(), + package: RefCell::new(build.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["hi".to_string()], + }); + + input.resolve(); + + child + .enabled_features + .extend(["one".to_string(), "two".to_string(), "three".to_string()]); + build + .enabled_features + .extend(["hi".to_string(), "world".to_string()]); + + let mut expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + expected.build_dependencies.push(Dependency { + name: "build".to_string(), + package: RefCell::new(build.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["hi".to_string()], + }); + + assert_eq!(input, expected); + } + + // Dependencies behind a feature should be enabled + #[test] + fn feature_dependency() { + let mut optional = make_package_node("optional", vec![("feature", vec![])], None); + let mut optional_build = + make_package_node("optional", vec![("build_feature", vec![])], None); + + let mut child = make_package_node( + "child", + vec![ + ("one", vec!["optional"]), + ("optional", vec!["dep:optional"]), + ], + Some(Dependency { + name: "optional".to_string(), + package: RefCell::new(optional.clone()).into(), + optional: true, + uses_default_features: true, + features: vec!["feature".to_string()], + }), + ); + let mut build = make_package_node( + "build", + vec![("hi", vec!["dep:optional"])], + Some(Dependency { + name: "optional".to_string(), + package: RefCell::new(optional_build.clone()).into(), + optional: true, + uses_default_features: true, + features: vec!["build_feature".to_string()], + }), + ); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + input.build_dependencies.push(Dependency { + name: "build".to_string(), + package: RefCell::new(build.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["hi".to_string()], + }); + + input.resolve(); + + optional.enabled_features.extend(["feature".to_string()]); + optional_build + .enabled_features + .extend(["build_feature".to_string()]); + + child.dependencies[0].optional = false; + child.dependencies[0].package = RefCell::new(optional).into(); + child + .enabled_features + .extend(["one".to_string(), "optional".to_string()]); + + build.dependencies[0].optional = false; + build.dependencies[0].package = RefCell::new(optional_build).into(); + build.enabled_features.extend(["hi".to_string()]); + + let mut expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + expected.build_dependencies.push(Dependency { + name: "build".to_string(), + package: RefCell::new(build.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["hi".to_string()], + }); + + assert_eq!(input, expected); + } + + // Renamed dependencies behind a feature should be enabled + #[test] + fn feature_renamed_dependency() { + let rename = make_package_node("rename", vec![], None); + let build_rename = make_package_node("build_rename", vec![], None); + let mut child = make_package_node( + "child", + vec![ + ("new_name", vec!["dep:new_name"]), + ("new_build_name", vec!["dep:new_build_name"]), + ], + Some(Dependency { + name: "new_name".to_string(), + package: RefCell::new(rename.clone()).into(), + optional: true, + uses_default_features: true, + features: vec![], + }), + ); + child.build_dependencies.push(Dependency { + name: "new_build_name".to_string(), + package: RefCell::new(build_rename.clone()).into(), + optional: true, + uses_default_features: true, + features: vec![], + }); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["new_name".to_string(), "new_build_name".to_string()], + }), + ); + + input.resolve(); + + child.dependencies[0].optional = false; + child.dependencies[0].package = RefCell::new(rename).into(); + child.build_dependencies[0].optional = false; + child.build_dependencies[0].package = RefCell::new(build_rename).into(); + child + .enabled_features + .extend(["new_name".to_string(), "new_build_name".to_string()]); + + let expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["new_name".to_string(), "new_build_name".to_string()], + }), + ); + + assert_eq!(input, expected); + } + + // Features on dependencies behind a feature should be enabled + #[test] + fn feature_dependency_features() { + let optional = make_package_node("optional", vec![("feature", vec![])], None); + let build_optional = + make_package_node("build_optional", vec![("build_feature", vec![])], None); + let mut child = make_package_node( + "child", + vec![ + ( + "one", + vec!["optional/feature", "build_optional/build_feature"], + ), + ("optional", vec!["dep:optional"]), + ("build_optional", vec!["dep:build_optional"]), + ], + Some(Dependency { + name: "optional".to_string(), + package: RefCell::new(optional.clone()).into(), + optional: true, + uses_default_features: true, + features: vec![], + }), + ); + child.build_dependencies.push(Dependency { + name: "build_optional".to_string(), + package: RefCell::new(build_optional.clone()).into(), + optional: true, + uses_default_features: true, + features: vec![], + }); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + + input.resolve(); + + child.dependencies[0].optional = false; + child.dependencies[0].features.push("feature".to_string()); + child.dependencies[0].package = RefCell::new(optional).into(); + child.dependencies[0] + .package + .borrow_mut() + .enabled_features + .extend(["feature".to_string()]); + child.build_dependencies[0].optional = false; + child.build_dependencies[0] + .features + .push("build_feature".to_string()); + child.build_dependencies[0].package = RefCell::new(build_optional).into(); + child.build_dependencies[0] + .package + .borrow_mut() + .enabled_features + .extend(["build_feature".to_string()]); + child.enabled_features.extend([ + "one".to_string(), + "optional".to_string(), + "build_optional".to_string(), + ]); + + let expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + + assert_eq!(input, expected); + } + + // Default dependencies chain behind a feature should be enabled + #[test] + fn feature_dependency_defaults() { + let optional = make_package_node( + "optional", + vec![("default", vec!["std"]), ("std", vec![])], + None, + ); + let build_optional = make_package_node( + "optional", + vec![("default", vec!["build"]), ("build", vec![])], + None, + ); + let mut child = make_package_node( + "child", + vec![ + ("one", vec!["optional", "build_optional"]), + ("optional", vec!["dep:optional"]), + ("build_optional", vec!["dep:build_optional"]), + ], + Some(Dependency { + name: "optional".to_string(), + package: RefCell::new(optional.clone()).into(), + optional: true, + uses_default_features: true, + features: vec![], + }), + ); + child.build_dependencies.push(Dependency { + name: "build_optional".to_string(), + package: RefCell::new(build_optional.clone()).into(), + optional: true, + uses_default_features: true, + features: vec![], + }); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + + input.resolve(); + + child.dependencies[0].optional = false; + child.dependencies[0].package = RefCell::new(optional).into(); + child.dependencies[0] + .package + .borrow_mut() + .enabled_features + .extend(["std".to_string(), "default".to_string()]); + child.build_dependencies[0].optional = false; + child.build_dependencies[0].package = RefCell::new(build_optional).into(); + child.build_dependencies[0] + .package + .borrow_mut() + .enabled_features + .extend(["build".to_string(), "default".to_string()]); + child.enabled_features.extend([ + "one".to_string(), + "optional".to_string(), + "build_optional".to_string(), + ]); + + let expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + + assert_eq!(input, expected); + } + + // Default features on a dependency (with no-defaults) behind a feature should not be enabled + #[test] + fn feature_dependency_no_defaults() { + let optional = make_package_node( + "optional", + vec![("default", vec!["std"]), ("std", vec![])], + None, + ); + let build_optional = make_package_node( + "build_optional", + vec![("default", vec!["build"]), ("build", vec![])], + None, + ); + let mut child = make_package_node( + "child", + vec![ + ("one", vec!["optional", "build_optional"]), + ("optional", vec!["dep:optional"]), + ("build_optional", vec!["dep:build_optional"]), + ], + Some(Dependency { + name: "optional".to_string(), + package: RefCell::new(optional.clone()).into(), + optional: true, + uses_default_features: false, + features: vec![], + }), + ); + child.build_dependencies.push(Dependency { + name: "build_optional".to_string(), + package: RefCell::new(build_optional.clone()).into(), + optional: true, + uses_default_features: false, + features: vec![], + }); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + + input.resolve(); + + child.dependencies[0].optional = false; + child.dependencies[0].package = RefCell::new(optional).into(); + child.build_dependencies[0].optional = false; + child.build_dependencies[0].package = RefCell::new(build_optional).into(); + child.enabled_features.extend([ + "one".to_string(), + "optional".to_string(), + "build_optional".to_string(), + ]); + + let expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec!["one".to_string()], + }), + ); + + assert_eq!(input, expected); + } + + // Features on optional dependencies should be enabled if the dependency is enabled + #[test] + fn feature_on_optional_dependency() { + let optional = make_package_node( + "optional", + vec![("disabled", vec![]), ("enabled", vec![])], + None, + ); + let build_optional = make_package_node( + "build_optional", + vec![("build_disabled", vec![]), ("build_enabled", vec![])], + None, + ); + let mut child = make_package_node( + "child", + vec![ + ("optional", vec!["dep:optional"]), + ("build_optional", vec!["dep:build_optional"]), + ( + "hi", + vec!["optional?/enabled", "build_optional?/build_enabled"], + ), + ], + Some(Dependency { + name: "optional".to_string(), + package: RefCell::new(optional.clone()).into(), + optional: true, + uses_default_features: false, + features: vec![], + }), + ); + child.build_dependencies.push(Dependency { + name: "build_optional".to_string(), + package: RefCell::new(build_optional.clone()).into(), + optional: true, + uses_default_features: false, + features: vec![], + }); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec![ + "optional".to_string(), + "build_optional".to_string(), + "hi".to_string(), + ], + }), + ); + + input.resolve(); + + child.dependencies[0].optional = false; + child.dependencies[0].package = RefCell::new(optional).into(); + child.dependencies[0] + .package + .borrow_mut() + .enabled_features + .extend(["enabled".to_string()]); + child.dependencies[0].features = vec!["enabled".to_string()]; + child.build_dependencies[0].optional = false; + child.build_dependencies[0].package = RefCell::new(build_optional).into(); + child.build_dependencies[0] + .package + .borrow_mut() + .enabled_features + .extend(["build_enabled".to_string()]); + child.build_dependencies[0].features = vec!["build_enabled".to_string()]; + child.enabled_features.extend([ + "optional".to_string(), + "build_optional".to_string(), + "hi".to_string(), + ]); + + let expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "child".to_string(), + package: RefCell::new(child.clone()).into(), + optional: false, + uses_default_features: true, + features: vec![ + "optional".to_string(), + "build_optional".to_string(), + "hi".to_string(), + ], + }), + ); + + assert_eq!(input, expected); + } + + // Check that a no default dependency does not removing an existing default + // + // Imagine a child dependency that has two other crates dependant on it. The first crate has defaults turned on, + // and the second crate has defaults turned off. The the result of turning off the defaults should not override + // the already on defaults. + // + // Here is a graphical representation + // + // parent + // / \ + // / \ + // layer1_1 layer1_2 + // \ / + // (defaults) (no_defaults) + // \ / + // child + #[test] + fn no_default_correctly() { + let mut child = make_package_node( + "child", + vec![("default", vec!["std"]), ("other", vec!["who"])], + None, + ); + let child_rc = RefCell::new(child.clone()).into(); + + let layer1_1 = make_package_node( + "layer1_1", + vec![], + Some(Dependency { + name: "child".to_string(), + package: Rc::clone(&child_rc), + optional: false, + uses_default_features: true, + features: vec!["other".to_string()], + }), + ); + + let layer1_2 = make_package_node( + "layer1_2", + vec![], + Some(Dependency { + name: "child".to_string(), + package: Rc::clone(&child_rc), + optional: false, + uses_default_features: false, + features: vec!["other".to_string()], + }), + ); + + let mut input = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "layer1_1".to_string(), + package: RefCell::new(layer1_1).into(), + optional: false, + uses_default_features: true, + features: vec![], + }), + ); + input.dependencies.push(Dependency { + name: "layer1_2".to_string(), + package: RefCell::new(layer1_2).into(), + optional: false, + uses_default_features: true, + features: vec![], + }); + + input.resolve(); + + child.enabled_features.extend([ + "std".to_string(), + "default".to_string(), + "other".to_string(), + "who".to_string(), + ]); + + let child_rc = RefCell::new(child).into(); + + let layer1_1 = make_package_node( + "layer1_1", + vec![], + Some(Dependency { + name: "child".to_string(), + package: Rc::clone(&child_rc), + optional: false, + uses_default_features: true, + features: vec!["other".to_string()], + }), + ); + + let layer1_2 = make_package_node( + "layer1_2", + vec![], + Some(Dependency { + name: "child".to_string(), + package: Rc::clone(&child_rc), + optional: false, + uses_default_features: false, + features: vec!["other".to_string()], + }), + ); + + let mut expected = make_package_node( + "parent", + vec![], + Some(Dependency { + name: "layer1_1".to_string(), + package: RefCell::new(layer1_1).into(), + optional: false, + uses_default_features: true, + features: vec![], + }), + ); + expected.dependencies.push(Dependency { + name: "layer1_2".to_string(), + package: RefCell::new(layer1_2).into(), + optional: false, + uses_default_features: true, + features: vec![], + }); + + assert_eq!(input, expected); + } +} diff --git a/nbuild-core/src/models/mod.rs b/nbuild-core/src/models/mod.rs new file mode 100644 index 0000000..de9fb0f --- /dev/null +++ b/nbuild-core/src/models/mod.rs @@ -0,0 +1,623 @@ +//! Models to reason about the cargo inputs and the nix outputs + +use std::{cell::RefCell, collections::BTreeMap, path::PathBuf, rc::Rc}; + +use cargo_lock::Version; +use tracing::{instrument, trace}; + +pub mod cargo; +pub mod nix; + +/// Where does the crate's code come from +#[derive(Debug, PartialEq, Clone)] +pub enum Source { + /// It is a local path + /// + /// ```toml + /// [dependencies] + /// dependency = { path = "/local/path" } + /// ``` + Local(PathBuf), + + /// It is from crates.io + /// + /// ```toml + /// [dependencies] + /// dependency = "0.2.0" + /// ``` + CratesIo(String), +} + +/// Convert the cargo package to a nix package for output +impl From for nix::Package { + fn from(package: cargo::Package) -> Self { + let mut converted = Default::default(); + + let result = cargo_to_nix(package, &mut converted); + + // Drop what was converted so that we can unwrap from the Rc + drop(converted); + + Rc::try_unwrap(result).unwrap().into_inner() + } +} + +/// Recursively convert a cargo package to a nix package. Also ensure a crate is only converted once by using the +/// `converted` cache to lookup crates that have already been converted. +#[instrument(skip_all, fields(name = %cargo_package.name))] +fn cargo_to_nix( + cargo_package: cargo::Package, + converted: &mut BTreeMap<(String, Version), Rc>>, +) -> Rc> { + let cargo::Package { + name, + lib_name, + version, + source, + lib_path, + build_path, + proc_macro, + features: _, // We only care about the features that were enabled at the end + enabled_features, + dependencies, + build_dependencies, + edition, + } = cargo_package; + + match converted.get(&(name.clone(), version.clone())) { + Some(package) => Rc::clone(package), + None => { + let dependencies = dependencies + .iter() + .filter(|d| !d.optional) + .map(|dependency| convert_dependency(dependency, converted)) + .collect(); + let build_dependencies = build_dependencies + .iter() + .filter(|d| !d.optional) + .map(|dependency| convert_dependency(dependency, converted)) + .collect(); + + // Handle libs that rename themselves + let lib_name = lib_name.and_then(|n| if n == name { None } else { Some(n) }); + + // Handle libs with a custom `lib.rs` paths + let lib_path = lib_path.and_then(|p| if p == "src/lib.rs" { None } else { Some(p) }); + + // Handle custom `build.rs` paths + let build_path = build_path.and_then(|p| if p == "build.rs" { None } else { Some(p) }); + + // The features array needs to stay deterministic to prevent unneeded rebuilds, so we sort it + let mut features = enabled_features.into_iter().collect::>(); + features.sort(); + + let package = RefCell::new(nix::Package { + name: name.clone(), + version: version.clone(), + source, + lib_name, + lib_path, + build_path, + proc_macro, + features, + dependencies, + build_dependencies, + edition, + printed: false, + }) + .into(); + + converted.insert((name, version), Rc::clone(&package)); + + package + } + } +} + +fn convert_dependency( + dependency: &cargo::Dependency, + converted: &mut BTreeMap<(String, Version), Rc>>, +) -> nix::Dependency { + let cargo_package = Rc::clone(&dependency.package).borrow().clone(); + let package = cargo_to_nix(cargo_package, converted); + + let rename = if dependency.name == package.borrow().name { + None + } else { + trace!(dependency_name = dependency.name, "activating rename"); + + Some(dependency.name.to_string()) + }; + + nix::Dependency { package, rename } +} + +#[cfg(test)] +mod tests { + use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + path::PathBuf, + rc::Rc, + str::FromStr, + }; + + use crate::models::{cargo, nix}; + + use pretty_assertions::assert_eq; + + #[test] + fn cargo_to_nix() { + let workspace = PathBuf::from_str(env!("CARGO_MANIFEST_DIR")) + .unwrap() + .join("tests") + .join("workspace"); + let path = workspace.join("parent"); + + let libc = RefCell::new(cargo::Package { + name: "libc".to_string(), + version: "0.2.144".parse().unwrap(), + source: "libc_sha".into(), + lib_name: Some("libc".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: Some("build.rs".into()), + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("std".to_string(), vec![]), + ("default".to_string(), vec!["std".to_string()]), + ("use_std".to_string(), vec!["std".to_string()]), + ("extra_traits".to_string(), vec![]), + ("align".to_string(), vec![]), + ( + "rustc-dep-of-std".to_string(), + vec!["align".to_string(), "rustc-std-workspace-core".to_string()], + ), + ("const-extern-fn".to_string(), vec![]), + ( + "rustc-std-workspace-core".to_string(), + vec!["dep:rustc-std-workspace-core".to_string()], + ), + ]), + enabled_features: Default::default(), + edition: "2015".to_string(), + }) + .into(); + let optional = RefCell::new(cargo::Package { + name: "optional".to_string(), + version: "1.0.0".parse().unwrap(), + source: "optional_sha".into(), + lib_name: Some("optional".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("std".to_string(), vec![]), + ("default".to_string(), vec!["std".to_string()]), + ]), + enabled_features: Default::default(), + edition: "2021".to_string(), + }) + .into(); + + let input = cargo::Package { + name: "parent".to_string(), + lib_name: None, + version: "0.1.0".parse().unwrap(), + source: path.clone().into(), + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: vec![ + cargo::Dependency { + name: "child".to_string(), + package: RefCell::new(cargo::Package { + name: "child".to_string(), + version: "0.1.0".parse().unwrap(), + source: workspace.join("child").into(), + lib_name: Some("child".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: vec![ + cargo::Dependency { + name: "fnv".to_string(), + package: RefCell::new(cargo::Package { + name: "fnv".to_string(), + version: "1.0.7".parse().unwrap(), + source: "fnv_sha".into(), + lib_name: Some("fnv".to_string()), + lib_path: Some("lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("default".to_string(), vec!["std".to_string()]), + ("std".to_string(), vec![]), + ]), + enabled_features: Default::default(), + edition: "2015".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + cargo::Dependency { + name: "itoa".to_string(), + package: RefCell::new(cargo::Package { + name: "itoa".to_string(), + version: "1.0.6".parse().unwrap(), + source: "itoa_sha".into(), + lib_name: Some("itoa".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([( + "no-panic".to_string(), + vec!["dep:no-panic".to_string()], + )]), + enabled_features: Default::default(), + edition: "2018".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + cargo::Dependency { + name: "libc".to_string(), + package: Rc::clone(&libc), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + cargo::Dependency { + name: "optional".to_string(), + package: Rc::clone(&optional), + optional: true, + uses_default_features: true, + features: Default::default(), + }, + cargo::Dependency { + name: "new_name".to_string(), + package: RefCell::new(cargo::Package { + name: "rename".to_string(), + version: "0.1.0".parse().unwrap(), + source: workspace.join("rename").into(), + lib_name: Some("lib_rename".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + enabled_features: Default::default(), + edition: "2021".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + cargo::Dependency { + name: "rustversion".to_string(), + package: RefCell::new(cargo::Package { + name: "rustversion".to_string(), + version: "1.0.12".parse().unwrap(), + source: "rustversion_sha".into(), + lib_name: Some("rustversion".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: Some("build/build.rs".into()), + proc_macro: true, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + enabled_features: Default::default(), + edition: "2018".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + ], + build_dependencies: vec![cargo::Dependency { + name: "arbitrary".to_string(), + package: RefCell::new(cargo::Package { + name: "arbitrary".to_string(), + version: "1.3.0".parse().unwrap(), + source: "arbitrary_sha".into(), + lib_name: Some("arbitrary".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("derive".to_string(), vec!["derive_arbitrary".to_string()]), + ( + "derive_arbitrary".to_string(), + vec!["dep:derive_arbitrary".to_string()], + ), + ]), + enabled_features: Default::default(), + edition: "2018".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }], + features: HashMap::from([ + ( + "default".to_string(), + vec!["one".to_string(), "two".to_string()], + ), + ("one".to_string(), vec!["new_name".to_string()]), + ("two".to_string(), vec![]), + ("new_name".to_string(), vec!["dep:new_name".to_string()]), + ]), + enabled_features: HashSet::from([ + "one".to_string(), + "new_name".to_string(), + ]), + edition: "2021".to_string(), + }) + .into(), + optional: false, + uses_default_features: false, + features: vec!["one".to_string()], + }, + cargo::Dependency { + name: "itoa".to_string(), + package: RefCell::new(cargo::Package { + name: "itoa".to_string(), + version: "0.4.8".parse().unwrap(), + source: "itoa_sha".into(), + lib_name: Some("itoa".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("default".to_string(), vec!["std".to_string()]), + ("no-panic".to_string(), vec!["dep:no-panic".to_string()]), + ("std".to_string(), vec![]), + ("i128".to_string(), vec![]), + ]), + enabled_features: Default::default(), + edition: "2018".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: Default::default(), + }, + cargo::Dependency { + name: "libc".to_string(), + package: libc, + optional: false, + uses_default_features: true, + features: Default::default(), + }, + cargo::Dependency { + name: "optional".to_string(), + package: optional, + optional: true, + uses_default_features: true, + features: Default::default(), + }, + cargo::Dependency { + name: "targets".to_string(), + package: RefCell::new(cargo::Package { + name: "targets".to_string(), + version: "0.1.0".parse().unwrap(), + source: workspace.join("targets").into(), + lib_name: Some("targets".to_string()), + lib_path: Some("src/lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: HashMap::from([ + ("unix".to_string(), vec![]), + ("windows".to_string(), vec![]), + ]), + enabled_features: HashSet::from(["unix".to_string()]), + edition: "2021".to_string(), + }) + .into(), + optional: false, + uses_default_features: true, + features: vec!["unix".to_string()], + }, + ], + build_dependencies: Default::default(), + features: Default::default(), + enabled_features: Default::default(), + edition: "2021".to_string(), + }; + + let actual: nix::Package = input.into(); + + let libc = RefCell::new(nix::Package { + name: "libc".to_string(), + version: "0.2.144".parse().unwrap(), + source: "libc_sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2015".to_string(), + printed: false, + }) + .into(); + let expected = nix::Package { + name: "parent".to_string(), + version: "0.1.0".parse().unwrap(), + source: path.into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: vec![ + nix::Package { + name: "child".to_string(), + version: "0.1.0".parse().unwrap(), + source: workspace.join("child").into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: vec![ + nix::Package { + name: "fnv".to_string(), + version: "1.0.7".parse().unwrap(), + source: "fnv_sha".into(), + lib_name: None, + lib_path: Some("lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2015".to_string(), + printed: false, + } + .into(), + nix::Package { + name: "itoa".to_string(), + version: "1.0.6".parse().unwrap(), + source: "itoa_sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into(), + nix::Dependency { + package: Rc::clone(&libc), + rename: None, + }, + nix::Dependency { + package: RefCell::new(nix::Package { + name: "rename".to_string(), + version: "0.1.0".parse().unwrap(), + source: workspace.join("rename").into(), + lib_name: Some("lib_rename".to_string()), + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2021".to_string(), + printed: false, + }) + .into(), + rename: Some("new_name".to_string()), + }, + nix::Package { + name: "rustversion".to_string(), + version: "1.0.12".parse().unwrap(), + source: "rustversion_sha".into(), + lib_name: None, + lib_path: None, + build_path: Some("build/build.rs".into()), + proc_macro: true, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into(), + ], + build_dependencies: vec![nix::Package { + name: "arbitrary".to_string(), + version: "1.3.0".parse().unwrap(), + source: "arbitrary_sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into()], + features: vec!["new_name".to_string(), "one".to_string()], + edition: "2021".to_string(), + printed: false, + } + .into(), + nix::Package { + name: "itoa".to_string(), + version: "0.4.8".parse().unwrap(), + source: "itoa_sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into(), + nix::Dependency { + package: libc, + rename: None, + }, + nix::Package { + name: "targets".to_string(), + version: "0.1.0".parse().unwrap(), + source: workspace.join("targets").into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: vec!["unix".to_string()], + edition: "2021".to_string(), + printed: false, + } + .into(), + ], + build_dependencies: Default::default(), + features: Default::default(), + edition: "2021".to_string(), + printed: false, + }; + + assert_eq!(actual, expected); + + // Make sure the libcs are linked - ie, the version for both libcs should change with this one assignment + actual.dependencies[2].package.borrow_mut().version = "0.2.0".parse().unwrap(); + + assert_eq!( + actual.dependencies[2], + actual.dependencies[0].package.borrow().dependencies[2] + ); + } +} diff --git a/nbuild-core/src/models/nix.rs b/nbuild-core/src/models/nix.rs new file mode 100644 index 0000000..8e595de --- /dev/null +++ b/nbuild-core/src/models/nix.rs @@ -0,0 +1,834 @@ +//! This model is used to create / print a nix derivation. + +use std::{cell::RefCell, fs, rc::Rc}; + +use cargo_metadata::{camino::Utf8PathBuf, semver::Version}; + +use super::Source; + +/// A package for a nix [buildRustCrate] block. +/// +/// [buildRustCrate]: https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/rust.section.md#buildrustcrate-compiling-rust-crates-using-nix-instead-of-cargo-compiling-rust-crates-using-nix-instead-of-cargo +#[derive(Debug, PartialEq)] +pub struct Package { + pub(super) name: String, + pub(super) version: Version, + pub(super) source: Source, + pub(super) lib_name: Option, + pub(super) lib_path: Option, + pub(super) build_path: Option, + pub(super) proc_macro: bool, + pub(super) features: Vec, + pub(super) dependencies: Vec, + pub(super) build_dependencies: Vec, + pub(super) edition: String, + pub(super) printed: bool, +} + +/// Used to keep track of the dependencies of a package and whether they have any renames. +#[derive(Debug, PartialEq)] +pub struct Dependency { + pub(super) package: Rc>, + pub(super) rename: Option, +} + +impl Package { + /// Write the package to a derivation file at `.nbuild.nix` + pub fn into_file(self) -> Result<(), std::io::Error> { + let expr = self.into_derivative(); + + fs::write(".nbuild.nix", expr) + } + + /// Turn the package into a derivation string. + pub fn into_derivative(self) -> String { + let Self { + name, + version, + source, + lib_name: _, + lib_path: _, + build_path: _, + proc_macro: _, + features: _, + dependencies, + build_dependencies, + edition, + printed: _, + } = self; + + // Used to append all the dependency details unto + let mut build_details = Default::default(); + + let dep_idents: Vec<_> = dependencies + .into_iter() + .map(|d| { + let identifier = d.package.borrow().identifier(); + Self::to_details(&d, &mut build_details); + identifier + }) + .collect(); + + let build_deps = if build_dependencies.is_empty() { + Default::default() + } else { + let dep_idents: Vec<_> = build_dependencies + .into_iter() + .map(|d| { + let identifier = d.package.borrow().identifier(); + Self::to_details(&d, &mut build_details); + identifier + }) + .collect(); + format!("\n buildDependencies = [{}];", dep_idents.join(" ")) + }; + + format!( + r#"{{ pkgs ? import {{ + overlays = [ (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) ]; +}} }}: + +let + sourceFilter = name: type: + let + baseName = builtins.baseNameOf (builtins.toString name); + in + ! ( + # Filter out git + baseName == ".gitignore" + || (type == "directory" && baseName == ".git") + + # Filter out build results + || ( + type == "directory" && baseName == "target" + ) + + # Filter out nix-build result symlinks + || ( + type == "symlink" && pkgs.lib.hasPrefix "result" baseName + ) + ); + rustVersion = pkgs.rust-bin.stable."1.68.0".default; + defaultCrateOverrides = pkgs.defaultCrateOverrides // {{ + opentelemetry-proto = attrs: {{ buildInputs = [ pkgs.protobuf ]; }}; + }}; + fetchCrate = {{ crateName, version, sha256 }}: pkgs.fetchurl {{ + # https://www.pietroalbini.org/blog/downloading-crates-io/ + # Not rate-limited, CDN URL. + name = "${{crateName}}-${{version}}.tar.gz"; + url = "https://static.crates.io/crates/${{crateName}}/${{crateName}}-${{version}}.crate"; + inherit sha256; + }}; + buildRustCrate = pkgs.buildRustCrate.override {{ + rustc = rustVersion; + inherit defaultCrateOverrides fetchCrate; + }}; + preBuild = "rustc -vV"; + + # Core + {} = buildRustCrate rec {{ + crateName = "{}"; + version = "{}"; + + {} + + dependencies = [ + {} + ];{} + edition = "{}"; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }}; + + # Dependencies +{} +in +{} +"#, + name, + name, + version, + Self::get_source(&source), + dep_idents.join("\n "), + build_deps, + edition, + build_details.join("\n"), + name + ) + } + + /// Recursively add a dependency unto `details` + fn to_details(dependency: &Dependency, build_details: &mut Vec) { + let mut this = dependency.package.borrow_mut(); + + // Only print once + if this.printed { + return; + } + + let features = if this.features.is_empty() { + Default::default() + } else { + format!( + "\n features = [{}];", + this.features + .iter() + .map(|f| format!("\"{f}\"")) + .collect::>() + .join(" ") + ) + }; + + let lib_name = if let Some(lib_name) = &this.lib_name { + format!("\n libName = \"{lib_name}\";") + } else { + Default::default() + }; + let lib_path = if let Some(lib_path) = &this.lib_path { + format!("\n libPath = \"{lib_path}\";") + } else { + Default::default() + }; + let build_path = if let Some(build_path) = &this.build_path { + format!("\n build = \"{build_path}\";") + } else { + Default::default() + }; + let proc_macro = if this.proc_macro { + "\n procMacro = true;" + } else { + Default::default() + }; + + let mut renames = Vec::new(); + + let deps = if this.dependencies.is_empty() { + Default::default() + } else { + let dep_idents: Vec<_> = this + .dependencies + .iter() + .map(|d| { + if let Some(rename) = &d.rename { + renames.push(( + d.package.borrow().name.clone(), + rename.clone(), + d.package.borrow().version.to_string(), + )); + } + + d.package.borrow().identifier() + }) + .collect(); + format!("\n dependencies = [{}];", dep_idents.join(" ")) + }; + let build_deps = if this.build_dependencies.is_empty() { + Default::default() + } else { + let dep_idents: Vec<_> = this + .build_dependencies + .iter() + .map(|d| { + if let Some(rename) = &d.rename { + renames.push(( + d.package.borrow().name.clone(), + rename.clone(), + d.package.borrow().version.to_string(), + )); + } + + d.package.borrow().identifier() + }) + .collect(); + format!("\n buildDependencies = [{}];", dep_idents.join(" ")) + }; + + let crate_renames = if renames.is_empty() { + Default::default() + } else { + let renames = renames + .into_iter() + .map(|(name, rename, version)| { + format!("\"{name}\" = [{{ rename = \"{rename}\"; version = \"{version}\"; }}];") + }) + .collect::>() + .join(" "); + + format!("\n crateRenames = {{{renames}}};") + }; + + let details = format!( + r#" {} = buildRustCrate rec {{ + crateName = "{}";{} + version = "{}"; + + {}{}{}{}{}{}{}{} + edition = "{}"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }};"#, + this.identifier(), + this.name, + lib_name, + this.version, + Self::get_source(&this.source), + lib_path, + build_path, + proc_macro, + deps, + build_deps, + crate_renames, + features, + this.edition, + ); + + build_details.push(details); + + for dependency in this + .dependencies + .iter() + .chain(this.build_dependencies.iter()) + { + Self::to_details(dependency, build_details); + } + + this.printed = true; + } + + /// Helper to get a deterministic identifier for a package + fn identifier(&self) -> String { + format!( + "{}_{}", + self.name, + self.version.to_string().replace(['.', '+'], "_") + ) + } + + /// Helper to get the source definition + fn get_source(source: &Source) -> String { + match source { + Source::Local(path) => format!( + "src = pkgs.lib.cleanSourceWith {{ filter = sourceFilter; src = {}; }};", + path.display() + ), + Source::CratesIo(sha256) => format!("sha256 = \"{sha256}\";"), + } + } +} + +#[cfg(test)] +mod tests { + use std::{path::PathBuf, str::FromStr}; + + use super::*; + + use pretty_assertions::assert_eq; + + impl From for Dependency { + fn from(package: Package) -> Self { + Self { + package: Rc::new(RefCell::new(package)), + rename: None, + } + } + } + + impl From for Source { + fn from(path: PathBuf) -> Self { + Self::Local(path) + } + } + + impl From<&str> for Source { + fn from(sha: &str) -> Self { + Self::CratesIo(sha.to_string()) + } + } + + #[test] + fn simple_package() { + let package = Package { + name: "simple".to_string(), + version: "0.1.0".parse().unwrap(), + source: PathBuf::from_str("/cargo-nbuild/nbuild-core/tests/simple") + .unwrap() + .into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: vec![Package { + name: "itoa".to_string(), + version: "1.0.6".parse().unwrap(), + source: "itoa_sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into()], + build_dependencies: vec![Package { + name: "arbitrary".to_string(), + version: "1.3.0".parse().unwrap(), + source: "arbitrary_sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into()], + features: Default::default(), + edition: "2021".to_string(), + printed: false, + }; + + let actual = package.into_derivative(); + + assert_eq!( + actual, + r#"{ pkgs ? import { + overlays = [ (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) ]; +} }: + +let + sourceFilter = name: type: + let + baseName = builtins.baseNameOf (builtins.toString name); + in + ! ( + # Filter out git + baseName == ".gitignore" + || (type == "directory" && baseName == ".git") + + # Filter out build results + || ( + type == "directory" && baseName == "target" + ) + + # Filter out nix-build result symlinks + || ( + type == "symlink" && pkgs.lib.hasPrefix "result" baseName + ) + ); + rustVersion = pkgs.rust-bin.stable."1.68.0".default; + defaultCrateOverrides = pkgs.defaultCrateOverrides // { + opentelemetry-proto = attrs: { buildInputs = [ pkgs.protobuf ]; }; + }; + fetchCrate = { crateName, version, sha256 }: pkgs.fetchurl { + # https://www.pietroalbini.org/blog/downloading-crates-io/ + # Not rate-limited, CDN URL. + name = "${crateName}-${version}.tar.gz"; + url = "https://static.crates.io/crates/${crateName}/${crateName}-${version}.crate"; + inherit sha256; + }; + buildRustCrate = pkgs.buildRustCrate.override { + rustc = rustVersion; + inherit defaultCrateOverrides fetchCrate; + }; + preBuild = "rustc -vV"; + + # Core + simple = buildRustCrate rec { + crateName = "simple"; + version = "0.1.0"; + + src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/simple; }; + + dependencies = [ + itoa_1_0_6 + ]; + buildDependencies = [arbitrary_1_3_0]; + edition = "2021"; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + + # Dependencies + itoa_1_0_6 = buildRustCrate rec { + crateName = "itoa"; + version = "1.0.6"; + + sha256 = "itoa_sha"; + edition = "2018"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + arbitrary_1_3_0 = buildRustCrate rec { + crateName = "arbitrary"; + version = "1.3.0"; + + sha256 = "arbitrary_sha"; + edition = "2018"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; +in +simple +"# + ); + } + + #[test] + fn workspace() { + let base = PathBuf::from_str("/cargo-nbuild/nbuild-core/tests/workspace").unwrap(); + + let libc = RefCell::new(Package { + name: "libc".to_string(), + version: "0.2.144".parse().unwrap(), + source: "sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2015".to_string(), + printed: false, + }) + .into(); + + let package = Package { + name: "parent".to_string(), + version: "0.1.0".parse().unwrap(), + source: base.join("parent").into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: vec![ + Package { + name: "child".to_string(), + version: "0.1.0".parse().unwrap(), + source: base.join("child").into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: vec![ + Package { + name: "fnv".to_string(), + version: "1.0.7".parse().unwrap(), + source: "sha".into(), + lib_name: None, + lib_path: Some("lib.rs".into()), + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2015".to_string(), + printed: false, + } + .into(), + Package { + name: "itoa".to_string(), + version: "1.0.6".parse().unwrap(), + source: "sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into(), + Dependency { + package: Rc::clone(&libc), + rename: None, + }, + Dependency { + package: RefCell::new(Package { + name: "rename".to_string(), + version: "0.1.0".parse().unwrap(), + source: base.join("rename").into(), + lib_name: Some("lib_rename".to_string()), + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2021".to_string(), + printed: false, + }) + .into(), + rename: Some("new_name".to_string()), + }, + Package { + name: "rustversion".to_string(), + version: "1.0.12".parse().unwrap(), + source: "sha".into(), + lib_name: None, + lib_path: None, + build_path: Some("build/build.rs".into()), + proc_macro: true, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into(), + ], + build_dependencies: vec![Package { + name: "arbitrary".to_string(), + version: "1.3.0".parse().unwrap(), + source: "sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into()], + features: vec!["one".to_string()], + edition: "2021".to_string(), + printed: false, + } + .into(), + Package { + name: "itoa".to_string(), + version: "0.4.8".parse().unwrap(), + source: "sha".into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: Default::default(), + edition: "2018".to_string(), + printed: false, + } + .into(), + Dependency { + package: libc, + rename: None, + }, + Package { + name: "targets".to_string(), + version: "0.1.0".parse().unwrap(), + source: base.join("targets").into(), + lib_name: None, + lib_path: None, + build_path: None, + proc_macro: false, + dependencies: Default::default(), + build_dependencies: Default::default(), + features: vec!["unix".to_string()], + edition: "2021".to_string(), + printed: false, + } + .into(), + ], + build_dependencies: Default::default(), + features: Default::default(), + edition: "2021".to_string(), + printed: false, + }; + + let actual = package.into_derivative(); + + assert_eq!( + actual, + r#"{ pkgs ? import { + overlays = [ (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) ]; +} }: + +let + sourceFilter = name: type: + let + baseName = builtins.baseNameOf (builtins.toString name); + in + ! ( + # Filter out git + baseName == ".gitignore" + || (type == "directory" && baseName == ".git") + + # Filter out build results + || ( + type == "directory" && baseName == "target" + ) + + # Filter out nix-build result symlinks + || ( + type == "symlink" && pkgs.lib.hasPrefix "result" baseName + ) + ); + rustVersion = pkgs.rust-bin.stable."1.68.0".default; + defaultCrateOverrides = pkgs.defaultCrateOverrides // { + opentelemetry-proto = attrs: { buildInputs = [ pkgs.protobuf ]; }; + }; + fetchCrate = { crateName, version, sha256 }: pkgs.fetchurl { + # https://www.pietroalbini.org/blog/downloading-crates-io/ + # Not rate-limited, CDN URL. + name = "${crateName}-${version}.tar.gz"; + url = "https://static.crates.io/crates/${crateName}/${crateName}-${version}.crate"; + inherit sha256; + }; + buildRustCrate = pkgs.buildRustCrate.override { + rustc = rustVersion; + inherit defaultCrateOverrides fetchCrate; + }; + preBuild = "rustc -vV"; + + # Core + parent = buildRustCrate rec { + crateName = "parent"; + version = "0.1.0"; + + src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/workspace/parent; }; + + dependencies = [ + child_0_1_0 + itoa_0_4_8 + libc_0_2_144 + targets_0_1_0 + ]; + edition = "2021"; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + + # Dependencies + child_0_1_0 = buildRustCrate rec { + crateName = "child"; + version = "0.1.0"; + + src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/workspace/child; }; + dependencies = [fnv_1_0_7 itoa_1_0_6 libc_0_2_144 rename_0_1_0 rustversion_1_0_12]; + buildDependencies = [arbitrary_1_3_0]; + crateRenames = {"rename" = [{ rename = "new_name"; version = "0.1.0"; }];}; + features = ["one"]; + edition = "2021"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + fnv_1_0_7 = buildRustCrate rec { + crateName = "fnv"; + version = "1.0.7"; + + sha256 = "sha"; + libPath = "lib.rs"; + edition = "2015"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + itoa_1_0_6 = buildRustCrate rec { + crateName = "itoa"; + version = "1.0.6"; + + sha256 = "sha"; + edition = "2018"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + libc_0_2_144 = buildRustCrate rec { + crateName = "libc"; + version = "0.2.144"; + + sha256 = "sha"; + edition = "2015"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + rename_0_1_0 = buildRustCrate rec { + crateName = "rename"; + libName = "lib_rename"; + version = "0.1.0"; + + src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/workspace/rename; }; + edition = "2021"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + rustversion_1_0_12 = buildRustCrate rec { + crateName = "rustversion"; + version = "1.0.12"; + + sha256 = "sha"; + build = "build/build.rs"; + procMacro = true; + edition = "2018"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + arbitrary_1_3_0 = buildRustCrate rec { + crateName = "arbitrary"; + version = "1.3.0"; + + sha256 = "sha"; + edition = "2018"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + itoa_0_4_8 = buildRustCrate rec { + crateName = "itoa"; + version = "0.4.8"; + + sha256 = "sha"; + edition = "2018"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; + targets_0_1_0 = buildRustCrate rec { + crateName = "targets"; + version = "0.1.0"; + + src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/workspace/targets; }; + features = ["unix"]; + edition = "2021"; + crateBin = []; + codegenUnits = 16; + extraRustcOpts = [ "-C embed-bitcode=no" ]; + inherit preBuild; + }; +in +parent +"# + ); + } +} diff --git a/nbuild-core/tests/simple/Cargo.lock b/nbuild-core/tests/simple/Cargo.lock new file mode 100644 index 0000000..e06efc7 --- /dev/null +++ b/nbuild-core/tests/simple/Cargo.lock @@ -0,0 +1,23 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arbitrary" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "simple" +version = "0.1.0" +dependencies = [ + "arbitrary", + "itoa", +] diff --git a/nbuild-core/tests/simple/Cargo.toml b/nbuild-core/tests/simple/Cargo.toml new file mode 100644 index 0000000..800902d --- /dev/null +++ b/nbuild-core/tests/simple/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "simple" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +itoa = "1.0" + +[build-dependencies] +arbitrary = "1.3" diff --git a/nbuild-core/tests/simple/src/main.rs b/nbuild-core/tests/simple/src/main.rs new file mode 100644 index 0000000..f08fd21 --- /dev/null +++ b/nbuild-core/tests/simple/src/main.rs @@ -0,0 +1,7 @@ +fn main() { + let mut buffer = itoa::Buffer::new(); + let printed = buffer.format(128u64); + assert_eq!(printed, "128"); + + dbg!(printed); +} diff --git a/nbuild-core/tests/workspace/Cargo.lock b/nbuild-core/tests/workspace/Cargo.lock new file mode 100644 index 0000000..a43bc58 --- /dev/null +++ b/nbuild-core/tests/workspace/Cargo.lock @@ -0,0 +1,62 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "child" +version = "0.1.0" +dependencies = [ + "fnv", + "itoa 1.0.6", + "libc", + "rename", + "rustversion", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "libc" +version = "0.2.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" + +[[package]] +name = "parent" +version = "0.1.0" +dependencies = [ + "child", + "itoa 0.4.8", + "libc", + "targets", +] + +[[package]] +name = "rename" +version = "0.1.0" + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "targets" +version = "0.1.0" diff --git a/nbuild-core/tests/workspace/Cargo.toml b/nbuild-core/tests/workspace/Cargo.toml new file mode 100644 index 0000000..2acecd7 --- /dev/null +++ b/nbuild-core/tests/workspace/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] + +members = [ + "child", + "parent", + "rename", + "targets", +] diff --git a/nbuild-core/tests/workspace/child/Cargo.toml b/nbuild-core/tests/workspace/child/Cargo.toml new file mode 100644 index 0000000..75376b7 --- /dev/null +++ b/nbuild-core/tests/workspace/child/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "child" +version = "0.1.0" +edition = "2021" + +[features] +default = ["one", "two"] +one = ["new_name"] +two = [] + +[dependencies] +fnv = "1.0" # This dependency uses a custom lib path +itoa = "1.0" +libc = "0.2" +rustversion = "1.0" # This dependency uses a custom build path is a proc_macro +new_name = { path = "../rename", package = "rename", optional = true } diff --git a/nbuild-core/tests/workspace/child/src/lib.rs b/nbuild-core/tests/workspace/child/src/lib.rs new file mode 100644 index 0000000..1564b0a --- /dev/null +++ b/nbuild-core/tests/workspace/child/src/lib.rs @@ -0,0 +1,34 @@ +use fnv::FnvHashMap; + +#[cfg(feature = "one")] +pub fn one() -> u8 { + let result = 5; + let mut buffer = itoa::Buffer::new(); + let printed = buffer.format(result); + assert_eq!(printed, "5"); + + result +} + +#[cfg(feature = "two")] +pub fn one() -> u8 { + panic!("two should not be active") +} + +pub fn lib_path() -> FnvHashMap { + let mut map = FnvHashMap::default(); + map.insert(1, "one"); + map.insert(2, "two"); + + map +} + +#[rustversion::before(1.68)] +pub fn version() -> &'static str { + "< 1.68.0" +} + +#[rustversion::since(1.68)] +pub fn version() -> &'static str { + ">= 1.68.0" +} diff --git a/nbuild-core/tests/workspace/parent/Cargo.toml b/nbuild-core/tests/workspace/parent/Cargo.toml new file mode 100644 index 0000000..534b442 --- /dev/null +++ b/nbuild-core/tests/workspace/parent/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "parent" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +child = { path = "../child", default-features = false, features = ["one"] } +libc = "0.2" +itoa = "0.4" + +targets = { path = "../targets" } + +# We want to make sure that a second+ version of a dependency from targets are also processed correctly +[target.'cfg(windows)'.dependencies] +targets = { path = "../targets", features = ["windows"] } + +[target.'cfg(unix)'.dependencies] +targets = { path = "../targets", features = ["unix"] } + +[dev-dependencies] +# Multiple appearences originating from dev dependencies should not affect the normal dependencies +# Aka, when we target unix, then the windows feature should not be active in normal dependencies +targets = { path = "../targets", features = ["windows"] } diff --git a/nbuild-core/tests/workspace/parent/src/main.rs b/nbuild-core/tests/workspace/parent/src/main.rs new file mode 100644 index 0000000..afa18f6 --- /dev/null +++ b/nbuild-core/tests/workspace/parent/src/main.rs @@ -0,0 +1,6 @@ +fn main() { + let answer = child::one(); + let version = child::version(); + println!("Hello, {answer}"); + println!("I'm was compiled with version {version}"); +} diff --git a/nbuild-core/tests/workspace/rename/Cargo.toml b/nbuild-core/tests/workspace/rename/Cargo.toml new file mode 100644 index 0000000..f51a47f --- /dev/null +++ b/nbuild-core/tests/workspace/rename/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rename" +version = "0.1.0" +edition = "2021" + +[lib] +name = "lib_rename" + +[dependencies] diff --git a/nbuild-core/tests/workspace/rename/src/lib.rs b/nbuild-core/tests/workspace/rename/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/nbuild-core/tests/workspace/rename/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/nbuild-core/tests/workspace/targets/Cargo.toml b/nbuild-core/tests/workspace/targets/Cargo.toml new file mode 100644 index 0000000..f038980 --- /dev/null +++ b/nbuild-core/tests/workspace/targets/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "targets" +version = "0.1.0" +edition = "2021" + +[features] +unix = [] +windows = [] diff --git a/nbuild-core/tests/workspace/targets/src/lib.rs b/nbuild-core/tests/workspace/targets/src/lib.rs new file mode 100644 index 0000000..d8297cc --- /dev/null +++ b/nbuild-core/tests/workspace/targets/src/lib.rs @@ -0,0 +1,9 @@ +#[cfg(feature = "unix")] +pub fn target() -> &'static str { + "unix" +} + +#[cfg(feature = "windows")] +pub fn target() -> &'static str { + "windows" +} diff --git a/nbuild/Cargo.toml b/nbuild/Cargo.toml new file mode 100644 index 0000000..e7fd3e6 --- /dev/null +++ b/nbuild/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cargo-nbuild" +version = "0.1.1" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "A Rust builder that uses the nix package manager" +readme = "../README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nbuild-core = { path = "../nbuild-core", version = "0.1.0" } +tokio = { version = "1.28.1", features = ["io-util", "macros", "process", "rt-multi-thread"] } +tracing = { workspace = true } +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/nbuild/src/main.rs b/nbuild/src/main.rs new file mode 100644 index 0000000..3d8f6db --- /dev/null +++ b/nbuild/src/main.rs @@ -0,0 +1,59 @@ +use std::{env::current_dir, error::Error, process::Stdio}; + +use nbuild_core::models::{cargo, nix}; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + process::Command, +}; +use tracing_subscriber::prelude::*; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let fmt_layer = tracing_subscriber::fmt::layer().pretty().with_ansi(false); + let filter_layer = tracing_subscriber::EnvFilter::from_default_env(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .init(); + + let mut package = cargo::Package::from_current_dir(current_dir()?)?; + package.resolve(); + + let package: nix::Package = package.into(); + package.into_file()?; + + let mut cmd = Command::new("nix"); + cmd.args([ + "build", + "--file", + ".nbuild.nix", + "--max-jobs", + "auto", + "--cores", + "0", + ]) + .stdout(Stdio::piped()); + + let mut child = cmd.spawn()?; + let stdout = child.stdout.take().expect("to get handle on stdout"); + + let mut reader = BufReader::new(stdout).lines(); + + // Drive process forward + tokio::spawn(async move { + let status = child.wait().await.expect("build to finish"); + + if status.success() { + println!("Build done"); + } else { + println!("Build failed"); + } + }); + + while let Some(line) = reader.next_line().await.expect("to get line") { + println!("{line}"); + } + + Ok(()) +}