diff --git a/anneal/v2/Cargo.lock b/anneal/v2/Cargo.lock index 4a297be25c..b2001ee37e 100644 --- a/anneal/v2/Cargo.lock +++ b/anneal/v2/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -73,6 +82,30 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "base64" version = "0.22.1" @@ -94,24 +127,74 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + [[package]] name = "cargo-anneal" version = "0.1.0-alpha.22" dependencies = [ + "anyhow", + "cargo_metadata", "clap", + "clap-cargo", "env_logger", "exocrate", + "fs2", + "indicatif", "log", + "miette", "pathdiff", + "rayon", + "serde", "serde_json", + "sha2", "tempfile", + "thiserror", "toml_const", + "walkdir", +] + +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", ] [[package]] @@ -142,6 +225,19 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-cargo" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936551935c8258754bb8216aec040957d261f977303754b9bf1a213518388006" +dependencies = [ + "anstyle", + "cargo_metadata", + "clap", + "serde", + "serde_json", +] + [[package]] name = "clap_builder" version = "4.6.0" @@ -178,6 +274,18 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -196,6 +304,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -237,6 +370,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "env_filter" version = "1.0.1" @@ -339,6 +484,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -385,6 +554,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "hashbrown" version = "0.15.5" @@ -440,6 +615,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-segmentation", + "unicode-width 0.2.2", + "unit-prefix", + "web-time", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -486,6 +681,18 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -525,6 +732,36 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -535,6 +772,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -553,6 +799,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + [[package]] name = "pathdiff" version = "0.2.3" @@ -608,6 +860,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkg-config" version = "0.3.33" @@ -669,6 +927,26 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -723,6 +1001,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustix" version = "1.1.4" @@ -771,11 +1055,30 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -784,6 +1087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -863,6 +1167,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "strsim" version = "0.11.1" @@ -875,6 +1185,27 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "syn" version = "2.0.117" @@ -910,6 +1241,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1005,12 +1356,42 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "untrusted" version = "0.9.0" @@ -1064,6 +1445,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1088,6 +1479,51 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1122,6 +1558,16 @@ dependencies = [ "semver", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "1.0.7" @@ -1147,6 +1593,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/anneal/v2/Cargo.toml b/anneal/v2/Cargo.toml index ff669f7e7f..06ccc47b25 100644 --- a/anneal/v2/Cargo.toml +++ b/anneal/v2/Cargo.toml @@ -48,10 +48,21 @@ url = "https://example.com/macos-aarch64.tar.zst" exocrate = { path = "../../exocrate" } toml_const = "1.3.0" clap = { version = "4.5", features = ["derive"] } +clap-cargo = { version = "0.18.3", features = ["cargo_metadata"] } env_logger = "0.11" log = "0.4" +anyhow = "1.0.102" +cargo_metadata = "0.23.1" +miette = { version = "7.6.0", features = ["derive", "fancy"] } +thiserror = "2.0.18" +serde_json = "1.0.149" +serde = { version = "1.0.228", features = ["derive"] } +indicatif = { version = "0.18.4", features = ["improved_unicode"] } +fs2 = "0.4" +walkdir = "2.5.0" +sha2 = "0.10" +tempfile = "3.27.0" +rayon = "1.11.0" [dev-dependencies] pathdiff = "0.2" -serde_json = "1" -tempfile = "3" diff --git a/anneal/v2/chase-aeneas-versions.sh b/anneal/v2/chase-aeneas-versions.sh index 56ea54635a..c5e46f78ae 100755 --- a/anneal/v2/chase-aeneas-versions.sh +++ b/anneal/v2/chase-aeneas-versions.sh @@ -87,6 +87,7 @@ need() { need curl need sha256sum need tar +need gzip need zstd need strings need python3 @@ -145,6 +146,7 @@ if [[ ! -f "$charon_bin" ]]; then exit 1 fi +set +o pipefail commit_date=$( strings "$charon_bin" | grep -o 'rustc version .* ([0-9a-f]\{9\} [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\})' | @@ -152,20 +154,36 @@ commit_date=$( grep -o '[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' | tr -d '\n' ) +set -o pipefail -if [[ -z "$commit_date" ]]; then - echo "could not infer Rust commit date from charon binary" >&2 - exit 1 -fi - -rust_date=$( - python3 - "$commit_date" <<'PY' +rust_date= +if [[ -n "$commit_date" ]]; then + rust_date=$( + python3 - "$commit_date" <<'PY' import datetime import sys print((datetime.date.fromisoformat(sys.argv[1]) + datetime.timedelta(days=1)).isoformat()) PY -) + ) +fi + +if [[ -z "$rust_date" ]]; then + set +o pipefail + rust_date=$( + strings "$charon_bin" | + grep -o 'channel = "nightly-[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}"' | + head -n 1 | + sed -E 's/channel = "nightly-([^"]+)"/\1/' | + tr -d '\n' + ) + set -o pipefail +fi + +if [[ -z "$rust_date" ]]; then + echo "could not infer Rust toolchain date from charon binary" >&2 + exit 1 +fi lean_dir="$work_dir/lean-toolchain" mkdir -p "$lean_dir" @@ -177,7 +195,7 @@ lean_output_hash=$(hash_path_sri "$lean_dir") rust_dir="$work_dir/rust-toolchain" mkdir -p "$rust_dir" -for component in rustc rust-std rustc-dev llvm-tools miri; do +for component in cargo rustc rust-std rustc-dev llvm-tools miri; do archive="$work_dir/$component-nightly-$rust_platform.tar.gz" url="https://static.rust-lang.org/dist/$rust_date/$component-nightly-$rust_platform.tar.gz" download "$url" "$archive" @@ -187,7 +205,7 @@ for component in rustc rust-std rustc-dev llvm-tools miri; do tar -xzf "$archive" -C "$tmp_extract" top_dir=$(find "$tmp_extract" -mindepth 1 -maxdepth 1 -type d | head -n 1) comp_dir=$(find "$top_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1) - cp -R "$comp_dir"/. "$rust_dir"/ + cp -R "$comp_dir"/* "$rust_dir"/ done rust_src_archive="$work_dir/rust-src-nightly.tar.gz" @@ -197,7 +215,11 @@ rm -rf "$tmp_extract" mkdir -p "$tmp_extract" tar -xzf "$rust_src_archive" -C "$tmp_extract" top_dir=$(find "$tmp_extract" -mindepth 1 -maxdepth 1 -type d | head -n 1) -cp -R "$top_dir/rust-src"/. "$rust_dir"/ +cp -R "$top_dir/rust-src"/* "$rust_dir"/ +if [[ -d "$rust_dir/share/man" ]]; then + find "$rust_dir/share/man" -type f ! -name '*.gz' -exec gzip -n {} + +fi +chmod -R a+rX,a-w "$rust_dir" rust_output_hash=$(hash_path_sri "$rust_dir") mathlib_rev=$( diff --git a/anneal/v2/flake.nix b/anneal/v2/flake.nix index 3c0057ab66..433fcf5c64 100644 --- a/anneal/v2/flake.nix +++ b/anneal/v2/flake.nix @@ -42,10 +42,10 @@ rustDate = "2026-05-31"; leanVersion = "v4.30.0-rc2"; - rustToolchainSha256 = if system == "x86_64-linux" then "sha256-tdLBvDewiNTUKOdMJ1pkU7mPrUY0xTFOZWdG9dDNiAk=" - else if system == "aarch64-linux" then "sha256-5gGGsObb22cKc2beF5UWMEJN4Df4PM23hK0A4QJ/kEM=" - else if system == "x86_64-darwin" then "sha256-v3By/ilhfQEfNECMoNlMC0pQndo5Lq1CTTbjcsaXMPw=" - else if system == "aarch64-darwin" then "sha256-I8pM8VuoBc5R/4ZR3ZiuHmbQjn361QOXTTU+kD5B0p8=" + rustToolchainSha256 = if system == "x86_64-linux" then "sha256-MmvOgC3shIOVMWT1MTRajw8JuLwRk/P3LsmGVslNGKw=" + else if system == "aarch64-linux" then "sha256-gWFajI7TJyjslQLZm4VWBBsKA6nYe1lNQwrgUp2hwSA=" + else if system == "x86_64-darwin" then "sha256-dBLHRLo3omD7KRq0D8lzg6XiQfDKWOMD6YTrLQhEneo=" + else if system == "aarch64-darwin" then "sha256-X7ndqbjsmnjL6KZzNCxkVFJPzAsAjUqerD/wc1rxK5E=" else throw "Unsupported system: ${system}"; leanToolchainSha256 = if system == "x86_64-linux" then "sha256-o47cQjSLK5YL8YZ2raaj+mGAvvO+dIDfVeP2L+WoyMs=" @@ -151,6 +151,7 @@ " cp -r $comp_dir/* $out/" " rm -rf tmp_extract" "}" + "extract_component \"cargo\"" "extract_component \"rustc\"" "extract_component \"rust-std\"" "extract_component \"rustc-dev\"" @@ -616,6 +617,7 @@ aeneas/packages/mathlib/lake-manifest.json \ aeneas/packages/mathlib/.lake/config/mathlib/lakefile.olean \ lean/bin/lean \ + rust/bin/cargo \ rust/bin/rustc; do if ! grep -Fxq "$path" "$TMPDIR/archive/entries"; then echo "ERROR: expected archive entry missing: $path" >&2 diff --git a/anneal/v2/src/diagnostics.rs b/anneal/v2/src/diagnostics.rs new file mode 100644 index 0000000000..78e510318f --- /dev/null +++ b/anneal/v2/src/diagnostics.rs @@ -0,0 +1,271 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under the 2-Clause BSD License , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +// Handling of compiler diagnostics and source mapping. +// +// This module provides the [`crate::diagnostics::DiagnosticMapper`] struct, which is responsible +// for translating diagnostics from external tools, such as Charon, back to +// annotated Rust source code. It maps errors in generated intermediate files +// back to their origin spans in the user's codebase. + +pub use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel, DiagnosticSpan}; + +/// Maps diagnostics from generated intermediate code back to Rust source code. +/// +/// The Charon compiler has no knowledge of the Rust workspace layout that +/// orchestrated its execution. It reports diagnostics against generated or +/// rewritten compilation inputs. +/// +/// This mapper cross-references the compiler's emitted byte spans to +/// annotations in Rust source code, dynamically synthesizing a +/// [`miette::NamedSource`] that points into the user's `.rs` workspace files. +pub struct DiagnosticMapper { + user_root: std::path::PathBuf, + user_root_canonical: std::path::PathBuf, + source_cache: std::collections::HashMap, +} + +#[derive(thiserror::Error, Debug)] +#[error("{message}")] +struct MappedError { + message: String, + src: miette::NamedSource, + labels: Vec, + help: Option, + related: Vec, + severity: Option, +} + +impl miette::Diagnostic for MappedError { + fn source_code(&self) -> Option<&dyn miette::SourceCode> { + Some(&self.src) + } + + fn labels(&self) -> Option + '_>> { + if self.labels.is_empty() { None } else { Some(Box::new(self.labels.iter().cloned())) } + } + + fn help(&self) -> Option> { + self.help.as_ref().map(|h| Box::new(h.clone()) as Box) + } + + fn related<'a>( + &'a self, + ) -> Option + 'a>> { + if self.related.is_empty() { + None + } else { + let iter = self.related.iter().map(|e| e as &dyn miette::Diagnostic); + Some(Box::new(iter)) + } + } + + fn severity(&self) -> Option { + self.severity + } +} + +impl DiagnosticMapper { + /// Creates a new mapper rooted at `user_root`. + pub fn new(user_root: std::path::PathBuf) -> Self { + let user_root_canonical = + std::fs::canonicalize(&user_root).unwrap_or_else(|_| user_root.clone()); + Self { user_root, user_root_canonical, source_cache: std::collections::HashMap::new() } + } + + /// Resolves a path relative to the user root, if applicable. + /// + /// This ensures we only report diagnostics for files within the user's + /// workspace, avoiding noise from dependencies or system files. + pub fn map_path(&self, path: &std::path::Path) -> Option { + let mut p = path.to_path_buf(); + if p.is_relative() { + p = self.user_root.join(p); + } + + p = { + let mut normalized = std::path::PathBuf::new(); + for component in p.components() { + let most_recent = normalized.components().next_back(); + match (component, most_recent) { + (std::path::Component::ParentDir, Some(std::path::Component::Normal(_))) => { + normalized.pop(); + } + (std::path::Component::CurDir, _) => {} + _ => normalized.push(component), + } + } + normalized + }; + + // Strategy B: Starts with user_root or user_root_canonical. + (p.starts_with(&self.user_root) || p.starts_with(&self.user_root_canonical)).then_some(p) + } + + fn get_source(&mut self, path: &std::path::Path) -> Option { + if let Some(src) = self.source_cache.get(path) { + return Some(src.clone()); + } + if let Ok(src) = std::fs::read_to_string(path) { + self.source_cache.insert(path.to_path_buf(), src.clone()); + Some(src) + } else { + None + } + } + + /// Renders a diagnostic (from Cargo or Charon) using `miette`. + /// + /// This is strictly for rendering errors native to Rust processing (where + /// the structured error originates from our `syn` parser or Charon's + /// processing), bringing them into a unified, colorized `miette` format. + pub fn render_miette(&mut self, diag: &Diagnostic, mut printer: F) + where + F: FnMut(String), + { + let mut mapped_paths_and_spans: std::collections::HashMap< + std::path::PathBuf, + Vec<&DiagnosticSpan>, + > = std::collections::HashMap::new(); + + // 1) Group spans by mapped path. + for s in &diag.spans { + let p = std::path::PathBuf::from(&s.file_name); + if let Some(mapped_path) = self.map_path(&p) { + mapped_paths_and_spans.entry(mapped_path).or_default().push(s); + } + } + + // TODO: Should we be collecting all of them, not just the first? + let help_msg = diag + .children + .iter() + .find(|child| child.level == DiagnosticLevel::Help) + .map(|child| child.message.clone()); + + if !mapped_paths_and_spans.is_empty() { + // Find the path that contains the primary span, or just take the + // first one. + let primary_path = diag + .spans + .iter() + .find(|s| s.is_primary) + .and_then(|s| self.map_path(&std::path::PathBuf::from(&s.file_name))) + .or_else(|| mapped_paths_and_spans.keys().next().cloned()); + + if let Some(main_path) = primary_path { + let mut all_errors = Vec::new(); + + // Sort the paths to have the primary path first. + let mut paths: Vec = + mapped_paths_and_spans.keys().cloned().collect(); + paths.sort_by_key(|p| p != &main_path); + + for p in paths { + if let Some(src) = self.get_source(&p) { + let labels: Vec<_> = mapped_paths_and_spans + .get(&p) + .unwrap() + .iter() + .filter_map(|s| { + let label_text = s.label.clone().unwrap_or_default(); + let start: usize = s.byte_start.try_into().unwrap(); + let len = (s.byte_end - s.byte_start).try_into().unwrap(); + (start <= src.len() && start + len <= src.len()).then(|| { + let offset = miette::SourceOffset::from(start); + miette::LabeledSpan::new(Some(label_text), offset.offset(), len) + }) + }) + .collect(); + + let err = MappedError { + message: if p == main_path { + diag.message.clone() + } else { + format!("related to: {}", p.display()) + }, + src: miette::NamedSource::new(p.to_string_lossy(), src), + labels, + help: if p == main_path { help_msg.clone() } else { None }, + related: Vec::new(), + severity: match diag.level { + DiagnosticLevel::Error | DiagnosticLevel::Ice => { + Some(miette::Severity::Error) + } + DiagnosticLevel::Warning => Some(miette::Severity::Warning), + _ => Some(miette::Severity::Advice), + }, + }; + all_errors.push(err); + } + } + + if !all_errors.is_empty() { + let mut main_err = all_errors.remove(0); + main_err.related = all_errors; + printer(format!("{:?}", miette::Report::new(main_err))); + return; + } + } + } + + // If we get here, no span was successfully mapped. + let prefix = match diag.level { + DiagnosticLevel::Error | DiagnosticLevel::Ice => "[External Error]", + DiagnosticLevel::Warning => "[External Warning]", + _ => "[External Info]", + }; + + if let Some(span) = diag.spans.first() { + printer(format!("{} {}:{}: {}", prefix, span.file_name, span.line_start, diag.message)); + } else { + printer(format!("{} {}", prefix, diag.message)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_map_path_traversal() { + let temp = tempfile::tempdir().unwrap(); + let user_root = temp.path().join("workspace"); + std::fs::create_dir(&user_root).unwrap(); + + // Create a symlink in the workspace pointing outside. + let outside = temp.path().join("outside"); + std::fs::create_dir(&outside).unwrap(); + std::os::unix::fs::symlink(&outside, user_root.join("symlink")).unwrap(); + + let mapper = DiagnosticMapper::new(user_root.clone()); + + let malicious_path = user_root.join("symlink/../passwd"); + + let mapped = mapper.map_path(&malicious_path); + + // The path normalization routine relies on lexical analysis. It + // normalizes `workspace/symlink/../passwd` into `workspace/passwd`. + // Even though a physical resolution of this path would point to + // `outside/passwd` via the symlink, the lexical flattening grounds it + // back into the workspace root. Because the mapped path is inside the + // tree, the logic considers it sound and explicitly strips the + // traversal. + // + // Conversely, attempts to escape the root directory directly using `..` + // without an internal symlink are trapped and rejected. + let escaped_path = user_root.join("../outside/passwd"); + assert_eq!(mapper.map_path(&escaped_path), None, "Path traversal should be rejected"); + + // The lexically normalized path inside the symlink safely maps to + // `workspace/passwd`. + assert_eq!(mapped, Some(user_root.join("passwd"))); + } +} diff --git a/anneal/v2/src/main.rs b/anneal/v2/src/main.rs index 0cb74addc8..872ce9ac97 100644 --- a/anneal/v2/src/main.rs +++ b/anneal/v2/src/main.rs @@ -57,10 +57,11 @@ fn setup_installation_dir(args: SetupArgs) -> std::path::PathBuf { None => exocrate::Source::Remote(REMOTE), }; - CONFIG + let (installation_dir, _) = CONFIG .resolve_installation_dir_or_install(location, source) // FIXME: Implement unified error reporting (e.g., via `anyhow`). - .expect("failed to resolve-or-install dependencies") + .expect("failed to resolve-or-install dependencies"); + installation_dir } fn setup(args: SetupArgs) { diff --git a/anneal/v2/src/resolve.rs b/anneal/v2/src/resolve.rs new file mode 100644 index 0000000000..17c9f94a39 --- /dev/null +++ b/anneal/v2/src/resolve.rs @@ -0,0 +1,507 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under the 2-Clause BSD License , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +//! Cargo package and target resolution for Anneal. +//! +//! This module resolves Cargo-shaped requests into explicit Anneal artifacts +//! instead of preserving only the original Cargo flags for a later +//! `charon cargo` command. Cargo target flags can select different rustc units +//! depending on package layout: default selection may include both libs and +//! bins, `--tests` can build test harnesses plus other targets, and a library +//! target can expose multiple crate types such as `rlib` and `cdylib`. +//! +//! Keeping explicit artifact identity lets later stages assign one LLBC path +//! and one Charon configuration to each artifact. A single Charon invocation +//! for several artifacts would need to account for shared-output hazards: +//! several driver invocations can race on one `--dest-file`, Charon directory +//! output is named by Rust crate name and can collide across workspace members, +//! and Charon options such as `--start-from` or `--opaque` apply to every +//! target selected by that invocation. For example, two packages may both have +//! `[lib] name = "common_utils"`, or a request like `--lib --bin app` may +//! select a lib and bin that need different output paths and entry points. + +use anyhow::Context as _; +use sha2::Digest as _; + +#[derive(clap::Parser, Debug)] +pub struct Args { + #[command(flatten)] + pub manifest: clap_cargo::Manifest, + + #[command(flatten)] + pub workspace: clap_cargo::Workspace, + + #[command(flatten)] + pub features: clap_cargo::Features, + + /// Verify the library target. + #[arg(long)] + pub lib: bool, + + /// Verify specific binary targets. + #[arg(long)] + pub bin: Vec, + + /// Verify all binary targets. + #[arg(long)] + pub bins: bool, + + /// Verify specific example targets. + #[arg(long)] + pub example: Vec, + + /// Verify all example targets. + #[arg(long)] + pub examples: bool, + + /// Verify specific test targets. + #[arg(long)] + pub test: Vec, + + /// Verify all test targets. + #[arg(long)] + pub tests: bool, + + /// Allow `sorry` in proofs and inject `sorry` for missing proofs. + #[arg(long)] + pub allow_sorry: bool, + + /// Allow use of `isValid` annotations. + /// + /// `isValid` annotations are currently unsound. In particular, Rust does + /// not yet support annotating a field with `unsafe`, denoting that it + /// carries an invariant. Thus, `isValid` annotations are effectively + /// advisory – any code which does not have a Anneal annotation can modify + /// invariant-carrying fields without needing to use an `unsafe` block. + /// Without an `unsafe` block, Anneal has no way of knowing that an + /// operation needs to be analyzed for soundness. + /// + /// Once the `syn` parser supports parsing `unsafe` fields (which are + /// already supported in a nightly Rust feature), Anneal will require that + /// `isValid` is only used on `unsafe` fields. + #[arg(long)] + pub unsound_allow_is_valid: bool, +} + +#[derive(clap::Parser, Debug)] +pub struct SetupArgs {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum AnnealTargetKind { + /// A library target (generic). + Lib, + /// A Rust library. + RLib, + /// A procedural macro library. + ProcMacro, + /// A C-compatible dynamic library. + CDyLib, + /// A dynamic Rust library. + DyLib, + /// A static system library. + StaticLib, + /// A binary executable. + Bin, + /// A documentation example. + Example, + /// An integration test. + Test, +} + +impl AnnealTargetKind { + pub fn is_lib(&self) -> bool { + use AnnealTargetKind::*; + matches!(self, Lib | RLib | ProcMacro | CDyLib | DyLib | StaticLib) + } +} + +impl std::convert::TryFrom<&cargo_metadata::TargetKind> for AnnealTargetKind { + type Error = (); + + fn try_from(kind: &cargo_metadata::TargetKind) -> anyhow::Result { + use cargo_metadata::TargetKind::*; + match kind { + Lib => Ok(Self::Lib), + RLib => Ok(Self::RLib), + ProcMacro => Ok(Self::ProcMacro), + CDyLib => Ok(Self::CDyLib), + DyLib => Ok(Self::DyLib), + StaticLib => Ok(Self::StaticLib), + Bin => Ok(Self::Bin), + Example => Ok(Self::Example), + Test => Ok(Self::Test), + // Need `_` because `TargetKind` is `#[non_exhaustive]`. + Bench | CustomBuild | _ => Err(()), + } + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct AnnealTargetName { + /// The Cargo package that owns this artifact. + pub package_name: cargo_metadata::PackageName, + /// The Cargo target name. + pub target_name: String, + /// The target kind compiled for this artifact. + /// + /// This is part of the identity because one Cargo target can expose + /// multiple crate types, and because later Charon and Lean outputs must not + /// rely on target names alone. + pub kind: AnnealTargetKind, +} + +/// A fully resolved target ready for verification. +/// +/// This struct bridges the gap between `cargo_metadata`'s view of the world +/// and Anneal's verification pipeline. It contains absolute paths to critical +/// files, ensuring that downstream tools (Scanner, Charon) don't need to resolve +/// paths relative to the CWD or workspace root again. +#[derive(Debug)] +pub struct AnnealTarget { + pub name: AnnealTargetName, + pub kind: AnnealTargetKind, + + /// Path to the `Cargo.toml` for this target. + pub manifest_path: std::path::PathBuf, +} + +#[derive(Debug)] +pub struct Roots { + pub workspace: std::path::PathBuf, + // E.g., `target/anneal`. + anneal_global_root: std::path::PathBuf, + // E.g., `target/anneal/`. + anneal_run_root: std::path::PathBuf, + pub roots: Vec, +} + +impl Roots { + pub fn lock_run_root(&self) -> anyhow::Result> { + let lock = crate::util::DirLock::lock_exclusive(self.anneal_run_root.clone())?; + Ok(LockedRoots { roots: self, anneal_run_root: lock, llbc_override: None }) + } + + pub fn cargo_target_dir(&self) -> std::path::PathBuf { + self.anneal_global_root.join("cargo_target") + } +} + +/// A wrapper around [`crate::resolve::Roots`] that proves the build lock is held. +/// +/// This struct is the *only* way to access paths within the Anneal build +/// directory (e.g., LLBC output, Lean generation). This enforces that all +/// filesystem operations are guarded by the `BuildLock`. +pub struct LockedRoots<'a> { + roots: &'a Roots, + anneal_run_root: crate::util::DirLock, + pub llbc_override: Option, +} + +impl<'a> LockedRoots<'a> { + pub fn llbc_root(&self) -> std::path::PathBuf { + if let Some(ref over) = self.llbc_override { + over.clone() + } else { + self.anneal_run_root.path.join("llbc") + } + } + + // We expose the Cargo target directory for convenience, as it is used + // by downstream tools like Charon to coordinate dependency artifacts. + pub fn cargo_target_dir(&self) -> std::path::PathBuf { + self.roots.cargo_target_dir() + } + + pub fn workspace(&self) -> &std::path::PathBuf { + &self.roots.workspace + } +} + +/// Resolves all verification roots. +/// +/// Each entry represents a distinct compilation artifact to be verified. +/// Keeping this artifact list explicit is deliberate: later Charon invocation +/// code should not have to rediscover which files a Cargo flag happened to +/// produce for a particular workspace shape. +pub fn resolve_roots(args: &Args, toolchain: &crate::setup::Toolchain) -> anyhow::Result { + log::trace!("resolve_roots({:?})", args); + let mut cmd = cargo_metadata::MetadataCommand::new(); + cmd.cargo_path(crate::setup::Tool::Cargo.path(toolchain)) + .env("RUSTC", crate::setup::Tool::Rustc.path(toolchain)) + .env(crate::setup::rust_library_path_env_var(), toolchain.rust_lib()); + + if let Some(path) = &args.manifest.manifest_path { + cmd.manifest_path(path); + } + + // Forward features to ensure dependency resolution matches the user's + // request. This is critical because conditional compilation + // (via `#[cfg(feature = "...")]`) can completely change the shape of + // the dependency graph and the source code itself. If we resolved + // metadata without these flags, we might miss dependencies or include + // dependencies that are not actually used in the build. + args.features.forward_metadata(&mut cmd); + + let metadata = cmd.exec().context("Failed to run 'cargo metadata'")?; + // We enforce that all local dependencies are contained within the workspace + // root. This is a temporary limitation to simplify the verification model + // and ensure a "hermetic-like" boundary for analysis. It prevents issues + // where experimental or local forks of dependencies might be picked up + // unpredictably, or where Charon might struggle to locate source files + // outside the standard project structure. + check_for_external_deps(&metadata)?; + + let selected_packages = + resolve_packages(&metadata, &args.workspace, args.manifest.manifest_path.as_deref())?; + + let (anneal_global_root, anneal_run_root) = resolve_run_roots(&metadata); + let mut roots = Roots { + workspace: metadata.workspace_root.as_std_path().to_owned(), + // cargo_target_dir: metadata.target_directory.as_std_path().to_owned(), + anneal_global_root, + anneal_run_root, + roots: Vec::new(), + }; + + for package in selected_packages { + log::trace!("Scanning package: {}", package.name); + + let targets = resolve_targets(package, args)?; + + if targets.is_empty() { + log::warn!("No matching targets found for package '{}'", package.name); + continue; + } + + roots.roots.extend(targets.into_iter().map(|(target, kind)| AnnealTarget { + name: AnnealTargetName { + package_name: package.name.clone(), + target_name: target.name.clone(), + kind, + }, + kind, + // We convert to absolute paths here to establish a canonical + // reference for the rest of the pipeline. This avoids ambiguity + // if the CWD changes or if we're working with complex workspace + // structures. + manifest_path: package.manifest_path.as_std_path().to_owned(), + })); + } + + Ok(roots) +} + +fn resolve_run_roots( + metadata: &cargo_metadata::Metadata, +) -> (std::path::PathBuf, std::path::PathBuf) { + log::trace!("resolve_run_root"); + log::debug!("workspace_root: {:?}", metadata.workspace_root.as_std_path()); + // NOTE: Automatically handles `CARGO_TARGET_DIR` env var. + let target_dir = metadata.target_directory.as_std_path(); + let anneal_global = target_dir.join("anneal"); + + // Used by integration tests to ensure deterministic shadow dir names. + if let Ok(name) = std::env::var("ANNEAL_TEST_DIR_NAME") { + let run_root = anneal_global.join(name); + return (anneal_global, run_root); + } + + // Hash the path to the workspace root to avoid collisions between different + // workspaces using the same target directory. We use SHA-256 (truncated to + // 64 bits) for stable hashing across Rust versions. This ensures that the + // build directory name remains consistent for the same workspace root, + // avoiding unnecessary cache invalidation. + let workspace_root_hash = { + let mut hasher = sha2::Sha256::new(); + hasher.update(b"anneal_build_salt"); + hasher.update(metadata.workspace_root.as_str().as_bytes()); + let result = hasher.finalize(); + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&result[0..8]); + u64::from_le_bytes(bytes) + }; + + let run_root = anneal_global.join(format!("{workspace_root_hash:x}")); + (anneal_global, run_root) +} + +/// Resolves which packages to process based on workspace flags and CWD. +fn resolve_packages<'a>( + metadata: &'a cargo_metadata::Metadata, + args: &clap_cargo::Workspace, + manifest_path: Option<&std::path::Path>, +) -> anyhow::Result> { + log::trace!("resolve_packages(workspace: {}, all: {})", args.workspace, args.all); + let mut packages = if !args.package.is_empty() { + // Resolve explicitly selected packages (-p / --package). + args.package + .iter() + .map(|name| { + metadata + .packages + .iter() + .find(|p| p.name == *name) + .ok_or_else(|| anyhow::anyhow!("Package '{}' not found in workspace", name)) + }) + .collect::>>()? + } else if args.workspace || args.all { + // Resolve entire workspace (--workspace / --all). This explicitly + // selects all workspace members, ignoring any packages that might be + // in the graph but are not members (e.g. dependencies). + metadata + .workspace_members + .iter() + .filter_map(|id| metadata.packages.iter().find(|p| &p.id == id)) + .collect() + } else { + // Resolve default (Current Working Directory). This mimics Cargo's + // behavior: if we are inside a package, we build that package. If we + // are at the workspace root, we build the whole workspace. + let cwd = { + let cwd_candidate = manifest_path + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()) + .canonicalize() + .context("Failed to canonicalize CWD")?; + + // If the user explicitly provided `--manifest-path`, Cargo treats + // its parent directory as the effective CWD. + if manifest_path.is_some() { + cwd_candidate.parent().unwrap_or(&cwd_candidate).to_path_buf() + } else { + cwd_candidate + } + }; + + // Find the package whose manifest directory is an ancestor of CWD. + let current_pkg = metadata.packages.iter().find(|p| { + let manifest_dir = p.manifest_path.parent().unwrap(); + cwd.starts_with(manifest_dir) + }); + + if let Some(pkg) = current_pkg { + vec![pkg] + } else { + // Fallback: If we are at the workspace root (virtual manifest), + // behave like --workspace. + if cwd == metadata.workspace_root.as_std_path() { + metadata + .workspace_members + .iter() + .filter_map(|id| metadata.packages.iter().find(|p| &p.id == id)) + .collect() + } else { + return Err(anyhow::anyhow!( + "Could not determine package from current directory. Please use -p or --workspace." + )); + } + } + }; + + // Filter out excluded packages (--exclude). + if !args.exclude.is_empty() { + packages.retain(|p| !args.exclude.contains(&p.name)); + } + + Ok(packages) +} + +/// Resolves the Cargo targets selected from one package. +/// +/// Returns a list of `(Target, TargetKind)` pairs. +/// If a target is defined as `crate-type = ["rlib", "cdylib"]`, and both are +/// requested, this returns two entries, allowing them to be verified +/// independently. +/// +/// This flattening is critical because different crate types may be compiled +/// with different flags or conditional compilation options (although the +/// current scanner is CFG-agnostic, future improvements might respect this). +/// Verifying them independently gives later stages a separate artifact identity +/// and output path for every intended compilation mode. +fn resolve_targets<'a>( + package: &'a cargo_metadata::Package, + args: &Args, +) -> anyhow::Result> { + log::trace!("resolve_targets({})", package.name); + let default_mode = !args.lib + && args.bin.is_empty() + && !args.bins + && args.example.is_empty() + && !args.examples + && args.test.is_empty() + && !args.tests; + + // Filter targets based on the requested verification mode. + // Unlike Cargo, which might build everything by default, we try to be + // selectively inclusive to avoid overwhelming the user with verification + // tasks they didn't ask for. + let selected_artifacts: Vec<_> = package + .targets + .iter() + .flat_map(|target| { + target.kind.iter().filter_map(move |raw_kind| { + let kind = AnnealTargetKind::try_from(raw_kind).ok()?; + + let include = if default_mode { + kind.is_lib() || kind == AnnealTargetKind::Bin + } else { + (args.lib && kind.is_lib()) + || (args.bins && kind == AnnealTargetKind::Bin) + || (args.bin.contains(&target.name) && kind == AnnealTargetKind::Bin) + || (args.examples && kind == AnnealTargetKind::Example) + || (args.example.contains(&target.name) + && kind == AnnealTargetKind::Example) + || (args.tests && kind == AnnealTargetKind::Test) + || (args.test.contains(&target.name) && kind == AnnealTargetKind::Test) + }; + + include.then_some((target, kind)) + }) + }) + .collect(); + + Ok(selected_artifacts) +} + +/// Scans the package graph to ensure all local dependencies are contained +/// within the workspace root. Returns an error if an external path dependency +/// is found. +pub fn check_for_external_deps(metadata: &cargo_metadata::Metadata) -> anyhow::Result<()> { + log::trace!("check_for_external_deps"); + // Canonicalize workspace root to handle symlinks correctly. + let workspace_root = std::fs::canonicalize(&metadata.workspace_root) + .context("Failed to canonicalize workspace root")?; + + for pkg in &metadata.packages { + // We only care about packages that are "local" (source is None). + // If source is Some(...), it's from crates.io or git, which is fine + // (handled by Cargo). + if pkg.source.is_none() { + let pkg_path = pkg.manifest_path.as_std_path(); + + // Canonicalize the package path for comparison. + let canonical_pkg_path = std::fs::canonicalize(pkg_path) + .with_context(|| format!("Failed to canonicalize path for package {}", pkg.name))?; + + // Check if the package lives outside the workspace tree. + if !canonical_pkg_path.starts_with(&workspace_root) { + anyhow::bail!( + "Unsupported external dependency: '{}' at {:?}.\n\ + Anneal currently only supports verifying workspaces where all local \ + dependencies are contained within the workspace root.", + pkg.name, + pkg_path + ); + } + } + } + + Ok(()) +} diff --git a/anneal/v2/src/scanner.rs b/anneal/v2/src/scanner.rs new file mode 100644 index 0000000000..3933334db5 --- /dev/null +++ b/anneal/v2/src/scanner.rs @@ -0,0 +1,116 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under the 2-Clause BSD License , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +use sha2::Digest as _; + +/// Represents a compilation target (artifact) that needs to be processed. +/// +/// Charon compiles the entire target to generate LLBC files. The generated +/// LLBC is the source of truth for processing Anneal annotations that affect +/// Aeneas code generation. +#[derive(Clone, Debug)] +pub struct AnnealArtifact { + pub name: crate::resolve::AnnealTargetName, + pub target_kind: crate::resolve::AnnealTargetKind, + /// The path to the crate's `Cargo.toml`. + pub manifest_path: std::path::PathBuf, +} + +impl AnnealArtifact { + /// Returns a unique, Lean-compatible artifact slug. + /// + /// Charon uses the slug as the file stem for the target's emitted LLBC + /// file. Later Aeneas/Lean stages can reuse the same Lean-compatible stem + /// when associating generated Lean code with this artifact. The slug is + /// guaranteed to be a valid Lean identifier (no hyphens), and is unique + /// based on the manifest path, target name, and target kind. + pub fn artifact_slug(&self) -> String { + fn hash(data: &[u8]) -> u64 { + // Use SHA-256 not for security but rather stability; Rust's + // `DefaultHasher` doesn't guarantee stability even across runs of + // the same binary. + let mut hasher = sha2::Sha256::new(); + hasher.update(data); + let result = hasher.finalize(); + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&result[0..8]); + u64::from_le_bytes(bytes) + } + + // Compute `hash([hash(manifest_path), hash(target_name), hash(target_kind)])` to + // distinguish between e.g. (manifest_path, target_name) = ("abc", "def") and + // ("ab", "cdef"), which would hash identically if we just hashed their + // concatenation. + // + // `ANNEAL_HASH_WITH_REMOVED_PREFIX` allows our integration test + // framework to strip the randomized sandbox prefix from the manifest + // path before hashing, ensuring deterministic hashes even when running + // in a sandboxed environment. + let mut manifest_path_to_hash = self.manifest_path.as_path(); + if let Ok(prefix) = std::env::var("ANNEAL_HASH_WITH_REMOVED_PREFIX") { + if let Ok(stripped) = self.manifest_path.strip_prefix(&prefix) { + manifest_path_to_hash = stripped; + } + } + let h0 = hash(manifest_path_to_hash.as_os_str().as_encoded_bytes()); + let h1 = hash(self.name.target_name.as_bytes()); + let h2 = hash(&[self.target_kind as u8]); + let hashes = [h0, h1, h2]; + let h = hash(&hashes.map(u64::to_ne_bytes).concat()); + + // Converts kebab-case -> PascalCase. + // We convert both package and target names to PascalCase to ensure + // the generated Lean module name is a valid and idiomatic Lean + // identifier, matching Aeneas's output format. + let to_pascal = |s: &str| { + s.split(['-', '_']) + .map(|segment| { + let mut chars = segment.chars(); + match chars.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + chars.as_str(), + } + }) + .collect::() + }; + + let pkg = to_pascal(self.name.package_name.as_str()); + let target = to_pascal(&self.name.target_name); + + // We use the hash to ensure uniqueness. + format!("{}{}{:08x}", pkg, target, h) + } + + /// Returns the name of the `.llbc` file to use for this artifact. + pub fn llbc_file_name(&self) -> String { + format!("{}.llbc", self.artifact_slug()) + } + + /// Returns the absolute path to the .llbc file. + /// + /// This method requires [`crate::resolve::LockedRoots`] to ensure that the caller holds the + /// build lock before accessing the build artifact path. + pub fn llbc_path(&self, roots: &crate::resolve::LockedRoots) -> std::path::PathBuf { + roots.llbc_root().join(self.llbc_file_name()) + } +} + +/// Scans the resolved workspace roots to identify the targets that need to be passed +/// to Charon. No Rust source code parsing is performed during this step. +pub fn scan_workspace(roots: &crate::resolve::Roots) -> anyhow::Result> { + let mut artifacts = Vec::new(); + for target in &roots.roots { + artifacts.push(AnnealArtifact { + name: target.name.clone(), + target_kind: target.kind, + manifest_path: target.manifest_path.clone(), + }); + } + Ok(artifacts) +} diff --git a/anneal/v2/src/setup.rs b/anneal/v2/src/setup.rs new file mode 100644 index 0000000000..9e17911f4d --- /dev/null +++ b/anneal/v2/src/setup.rs @@ -0,0 +1,154 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under the 2-Clause BSD License , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +use anyhow::Context as _; + +pub struct SetupArgs { + pub local_archive: Option, +} + +exocrate::config! { + pub const CONFIG: Config = Config { + rel_dir_path: [".anneal", "toolchain"], + versioned_files: &["../Cargo.toml", "../Cargo.lock"], + }; +} + +exocrate::parse_remote_archive! { + pub const REMOTE: RemoteArchive = "Cargo.toml" [ + (linux, x86_64), + (macos, x86_64), + (linux, aarch64), + (macos, aarch64), + ]; +} + +pub enum Tool { + Cargo, + Charon, + Rustc, +} + +impl Tool { + pub fn name(&self) -> &'static str { + match self { + Self::Cargo => "cargo", + Self::Charon => "charon", + Self::Rustc => "rustc", + } + } + + pub fn path(&self, toolchain: &Toolchain) -> std::path::PathBuf { + match self { + Self::Cargo | Self::Rustc => toolchain.rust_bin().join(self.name()), + Self::Charon => toolchain.aeneas_bin_dir().join(self.name()), + } + } +} + +const AENEAS_DIR: &str = "aeneas"; +const RUST_SYSROOT: &str = "rust"; +const AENEAS_BIN_DIR: &str = "bin"; +const RUST_BIN_DIR: &str = "bin"; +const RUST_LIB_DIR: &str = "lib"; + +pub struct Toolchain { + root: std::path::PathBuf, +} + +impl Toolchain { + pub fn resolve() -> anyhow::Result { + let location = resolve_location(); + let root = CONFIG + .resolve_installation_dir(location) + .context("Toolchain not installed. Please run 'cargo anneal setup' first.")?; + Ok(Self { root }) + } + + #[cfg(all(test, feature = "exocrate_tests"))] + pub fn root(&self) -> &std::path::Path { + &self.root + } + + pub fn aeneas_bin_dir(&self) -> std::path::PathBuf { + self.root.join(AENEAS_DIR).join(AENEAS_BIN_DIR) + } + + pub fn rust_sysroot(&self) -> std::path::PathBuf { + self.root.join(RUST_SYSROOT) + } + + pub fn rust_bin(&self) -> std::path::PathBuf { + self.rust_sysroot().join(RUST_BIN_DIR) + } + + pub fn rust_lib(&self) -> std::path::PathBuf { + self.rust_sysroot().join(RUST_LIB_DIR) + } + + pub fn command(&self, tool: Tool) -> anyhow::Result { + let mut cmd = std::process::Command::new(tool.path(self)); + cmd.env_clear(); + match tool { + Tool::Cargo | Tool::Rustc => {} + Tool::Charon => { + // The archive supplies Rust tools, but not host build tools + // such as the linker. Keep the caller's `PATH` after our Rust + // bin directory so Cargo builds use the managed Rust toolchain + // while still finding those host tools. + cmd.env("CHARON_TOOLCHAIN_IS_IN_PATH", "1") + .env("PATH", prepend_current_path(self.rust_bin())?) + .env(rust_library_path_env_var(), self.rust_lib()); + } + } + Ok(cmd) + } +} + +fn prepend_current_path(path: std::path::PathBuf) -> anyhow::Result { + let mut paths = vec![path]; + if let Some(current_path) = std::env::var_os("PATH") { + paths.extend(std::env::split_paths(¤t_path)); + } + std::env::join_paths(paths).context("failed to construct PATH for tool command") +} + +/// Returns the platform library search path variable used by Rust tools. +pub(crate) fn rust_library_path_env_var() -> &'static str { + if cfg!(target_os = "macos") { "DYLD_LIBRARY_PATH" } else { "LD_LIBRARY_PATH" } +} + +pub fn run_setup(args: SetupArgs) -> anyhow::Result<()> { + let location = resolve_location(); + let source = match args.local_archive { + Some(local_archive) => exocrate::Source::Local(local_archive), + None => exocrate::Source::Remote(REMOTE), + }; + + let (installation_dir, status) = CONFIG + .resolve_installation_dir_or_install(location, source) + .context("failed to resolve-or-install dependencies")?; + match status { + exocrate::ResolvedOrInstalled::ResolvedExisting => { + log::warn!("anneal toolchain was already installed at {:?}", installation_dir); + } + exocrate::ResolvedOrInstalled::NewlyInstalled => { + log::info!("anneal toolchain freshly installed at {:?}", installation_dir); + } + } + Ok(()) +} + +fn resolve_location() -> exocrate::Location { + if std::env::var("__ANNEAL_LOCAL_DEV").is_ok() { + exocrate::Location::LocalDev + } else { + exocrate::Location::UserGlobal + } +} diff --git a/anneal/v2/src/util.rs b/anneal/v2/src/util.rs new file mode 100644 index 0000000000..602e1585d1 --- /dev/null +++ b/anneal/v2/src/util.rs @@ -0,0 +1,283 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under the 2-Clause BSD License , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +use std::io::BufRead as _; + +use anyhow::Context as _; +use fs2::FileExt as _; + +/// Represents an active, exclusive lock on a directory. +/// +/// This struct guarantees that the process holds an OS-level file lock +/// guarding the specified directory. +pub(crate) struct DirLock { + /// The path to the directory being guarded. + pub(crate) path: std::path::PathBuf, + // Kept alive to hold the flock. + _file: std::fs::File, +} + +impl DirLock { + /// Acquires an exclusive lock on the specified directory. + /// + /// This function blocks until the lock can be acquired. We use a + /// separate `.lock` file within the directory rather than locking + /// the directory itself to avoid platform-specific issues with + /// directory locking and to ensure the lock file persists even if + /// the directory is cleaned. + pub(crate) fn lock_exclusive(path: std::path::PathBuf) -> anyhow::Result { + let file = Self::open_lock_file(&path)?; + file.lock_exclusive() + .with_context(|| format!("Failed to acquire exclusive lock on {:?}", path))?; + Ok(Self { path, _file: file }) + } + + /// Acquires a shared lock on the specified directory. + /// + /// Multiple processes can hold shared locks simultaneously, but an + /// exclusive lock will block until all shared locks are released. + #[cfg(any(test, feature = "exocrate_tests"))] + pub(crate) fn lock_shared(path: std::path::PathBuf) -> anyhow::Result { + let file = Self::open_lock_file(&path)?; + file.lock_shared() + .with_context(|| format!("Failed to acquire shared lock on {:?}", path))?; + Ok(Self { path, _file: file }) + } + + fn open_lock_file(path: &std::path::Path) -> anyhow::Result { + let lock_path = path.join(".lock"); + + // Ensure the directory exists. + if let Some(parent) = lock_path.parent() { + std::fs::create_dir_all(parent).with_context(|| { + format!("Failed to create directory for lock file: {:?}", parent) + })?; + } + // If the lock file already exists, we open it in read-only mode. + // This prevents failures if the file is read-only (e.g., after + // making the toolchain directory read-only), while still allowing + // us to acquire shared and exclusive locks on the file descriptor. + if lock_path.exists() { + std::fs::OpenOptions::new() + .read(true) + .open(&lock_path) + .with_context(|| format!("Failed to open lock file at {:?}", lock_path)) + } else { + std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&lock_path) + .with_context(|| format!("Failed to create lock file at {:?}", lock_path)) + } + } +} + +pub(crate) struct ProcessOutput { + pub status: std::process::ExitStatus, + pub stderr_lines: Vec, +} + +/// Spawns a child process, drains its stderr in a background thread, and processes +/// its stdout line-by-line in the main thread while showing a progress spinner. +pub(crate) fn run_command_with_progress( + mut cmd: std::process::Command, + pb: Option, + mut process_stdout_line: F, +) -> anyhow::Result +where + F: FnMut(&str, Option<&indicatif::ProgressBar>) -> anyhow::Result<()>, +{ + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let mut child = cmd.spawn().context("Failed to spawn child process")?; + + let stderr_buffer = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let stderr_buffer_clone = std::sync::Arc::clone(&stderr_buffer); + + let mut stderr_thread = None; + if let Some(stderr) = child.stderr.take() { + stderr_thread = Some(std::thread::spawn(move || { + let reader = std::io::BufReader::new(stderr); + for line in reader.lines().map_while(Result::ok) { + stderr_buffer_clone.lock().unwrap().push(line); + } + })); + } + + if let Some(ref p) = pb { + p.enable_steady_tick(std::time::Duration::from_millis(100)); + } + + if let Some(stdout) = child.stdout.take() { + let reader = std::io::BufReader::new(stdout); + for line in reader.lines().map_while(Result::ok) { + process_stdout_line(&line, pb.as_ref())?; + if let Some(ref p) = pb { + p.tick(); + } + } + } + + if let Some(ref p) = pb { + p.finish_and_clear(); + } + + let status = child.wait().context("Failed to wait for child process")?; + + if let Some(thread) = stderr_thread { + let _ = thread.join(); + } + + let stderr_lines = std::sync::Arc::try_unwrap(stderr_buffer).unwrap().into_inner().unwrap(); + + Ok(ProcessOutput { status, stderr_lines }) +} + +/// Performs a lock test action according to the `role` of the current actor. Actors may: +/// +/// - Obtain an exclusive or shared lock for `lock_dir`, +/// - Log actions in `log_file`, +/// - Wait for a signal from `sig_file`. +/// +/// Individual tests compose multiple role-based actions and verify the resulting action log. +#[cfg(feature = "exocrate_tests")] +pub(crate) fn run_test_lock_helper( + role: &str, + lock_dir: &std::path::Path, + log_file: &std::path::Path, + sig_file: &std::path::Path, +) -> anyhow::Result<()> { + use std::io::Write as _; + + let append_log = |msg: &str| -> anyhow::Result<()> { + let mut file = std::fs::OpenOptions::new().create(true).append(true).open(log_file)?; + writeln!(file, "{}", msg)?; + Ok(()) + }; + + let wait_for_sig = || -> anyhow::Result<()> { + let start = std::time::Instant::now(); + while !sig_file.exists() { + if start.elapsed() > std::time::Duration::from_secs(3) { + anyhow::bail!("Timeout waiting for signal file {:?}", sig_file); + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Ok(()) + }; + + match role { + "reader-a" => { + let _lock = DirLock::lock_shared(lock_dir.to_path_buf())?; + append_log("SHARED_START_A")?; + wait_for_sig()?; + append_log("SHARED_END_A")?; + } + "reader-b" => { + let _lock = DirLock::lock_shared(lock_dir.to_path_buf())?; + append_log("SHARED_START_B")?; + std::fs::write(sig_file, "")?; + append_log("SHARED_END_B")?; + } + "writer-a" => { + let _lock = DirLock::lock_exclusive(lock_dir.to_path_buf())?; + append_log("EXCLUSIVE_START_A")?; + wait_for_sig()?; + append_log("EXCLUSIVE_END_A")?; + } + "reader-exclusion" => { + std::fs::write(sig_file, "")?; + let _lock = DirLock::lock_shared(lock_dir.to_path_buf())?; + append_log("SHARED_START_B")?; + append_log("SHARED_END_B")?; + } + _ => anyhow::bail!("Unknown test-lock-helper role: {}", role), + } + + Ok(()) +} + +#[cfg(test)] +#[macro_export] +macro_rules! workspace_fixture { + ($dir:expr, { $($path:expr => $content:expr),* $(,)? }) => {{ + let root = $dir.path(); + $( + let file_path = root.join($path); + if let Some(parent) = file_path.parent() { + std::fs::create_dir_all(parent).unwrap(); + } + std::fs::write(&file_path, $content).unwrap(); + )* + }}; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dir_lock_exclusive_mutual_exclusion() { + let temp_dir = tempfile::tempdir().unwrap(); + let lock_path = temp_dir.path().to_path_buf(); + + let barrier = std::sync::Arc::new(std::sync::Barrier::new(2)); + let barrier_clone = std::sync::Arc::clone(&barrier); + let lock_path_clone = lock_path.clone(); + + let lock_released = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let lock_released_clone = std::sync::Arc::clone(&lock_released); + + // Thread A acquires the lock. + let thread_a = std::thread::spawn(move || { + let _lock = DirLock::lock_exclusive(lock_path_clone).expect("Failed to lock exclusive"); + barrier_clone.wait(); // Signal Thread B that A holds the lock. + + // Simulate brief work holding the lock. + std::thread::sleep(std::time::Duration::from_millis(100)); + lock_released_clone.store(true, std::sync::atomic::Ordering::Relaxed); + // _lock drops here, releasing the lock. + }); + + // Thread B waits for Thread A to acquire the lock, then tries to acquire it itself. + let thread_b = std::thread::spawn(move || { + barrier.wait(); // Wait for Thread A to acquire lock. + + // Attempt to acquire lock. This should block until Thread A releases it. + let _lock = DirLock::lock_exclusive(lock_path).expect("Failed to lock exclusive in B"); + + // Assert that B only successfully locked the directory AFTER A released it. + assert!( + lock_released.load(std::sync::atomic::Ordering::Relaxed), + "Thread B acquired lock before Thread A released it!" + ); + }); + + thread_a.join().unwrap(); + thread_b.join().unwrap(); + } + + #[test] + fn test_dir_lock_shared_coexistence() { + let temp_dir = tempfile::tempdir().unwrap(); + let lock_path = temp_dir.path().to_path_buf(); + + // Thread A acquires shared lock. + let lock_a = DirLock::lock_shared(lock_path.clone()).expect("Failed to lock shared"); + + // Thread B should be able to acquire shared lock immediately without blocking. + let lock_b = DirLock::lock_shared(lock_path).expect("Failed to lock shared concurrently"); + + // Both locks are held. + drop(lock_a); + drop(lock_b); + } +} diff --git a/exocrate/src/lib.rs b/exocrate/src/lib.rs index 3032fc4264..fdbf1efcbf 100644 --- a/exocrate/src/lib.rs +++ b/exocrate/src/lib.rs @@ -73,7 +73,7 @@ //! }; //! //! // Check whether `source` archive is already installed at `location`, and if not, install it. -//! let installed_exocrate_dir = CONFIG.resolve_installation_dir_or_install(location, source) +//! let (installed_exocrate_dir, _) = CONFIG.resolve_installation_dir_or_install(location, source) //! .expect("failed to resolve or install my-tool's exocrate"); //! //! // Invoke tool installed from `tests/my-tool-deps.tar.zst` extracted to versioned exocrate @@ -186,6 +186,15 @@ pub enum Source { Local(PathBuf), } +/// Whether an installation was resolved or newly installed. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ResolvedOrInstalled { + /// An existing installation was resolved. + ResolvedExisting, + /// A new installation was installed. + NewlyInstalled, +} + impl Config { /// Resolves the dependency directory, failing if it doesn't exist. pub fn resolve_installation_dir(&self, location: Location) -> IoResult { @@ -199,14 +208,14 @@ impl Config { &self, location: Location, source: Source, - ) -> IoResult { + ) -> IoResult<(PathBuf, ResolvedOrInstalled)> { let dir_path = self.dir_path(location)?; if ManagedDirName::new(&dir_path).check_exists().is_ok() { - return Ok(dir_path); + return Ok((dir_path, ResolvedOrInstalled::ResolvedExisting)); } let (reader, expected_sha) = self.open_source(source)?; install(reader, &dir_path, expected_sha)?; - Ok(dir_path) + Ok((dir_path, ResolvedOrInstalled::NewlyInstalled)) } /// Opens the given source. @@ -766,19 +775,21 @@ mod tests { let dev_path = config.dir_path(Location::LocalDev).unwrap(); let _ = fs::remove_dir_all(&dev_path); - let resolved = config + let (resolved, status) = config .resolve_installation_dir_or_install(Location::LocalDev, Source::Local(archive_path)) .unwrap(); assert_eq!(resolved, dev_path); + assert_eq!(status, ResolvedOrInstalled::NewlyInstalled); assert!(dev_path.join("bin/compiler").exists()); - let resolved2 = config + let (resolved2, status2) = config .resolve_installation_dir_or_install( Location::LocalDev, Source::Local(PathBuf::from("/nonexistent/path/should/not/be/accessed")), ) .unwrap(); assert_eq!(resolved2, dev_path); + assert_eq!(status2, ResolvedOrInstalled::ResolvedExisting); fs::remove_dir_all(&dev_path).unwrap(); }