diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cadeec13..0a768233c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Added** Remote caching. Configure an endpoint with the workspace's `cache: { remote: { url } }` or `VP_REMOTE_CACHE_URL`, and choose access with `--remote-cache=off|read|read-write` or `VP_REMOTE_CACHE`. The default is `read` with an endpoint and `off` without one. In `read-write` mode, `vp run` uploads the results of successful, cacheable tasks after caching them locally. A failed upload doesn't fail the task; the run summary shows a warning instead. Tasks can opt out with `cache: { remote: false }` ([#727](https://github.com/voidzero-dev/vite-task/pull/727), [#755](https://github.com/voidzero-dev/vite-task/pull/755)). - **Fixed** On Windows, environment variable names used by `vp run` now match regardless of ASCII letter case. Assignments in task commands override earlier assignments and inherited variables spelled differently, and `FORCE_COLOR`, `VP_RUN_CONCURRENCY_LIMIT`, and variables requested through `@voidzero-dev/vite-task-client` are found under any spelling ([#747](https://github.com/voidzero-dev/vite-task/pull/747)). - **Changed** A task's cache settings now go inside `cache`, e.g. `cache: { env: ["NODE_ENV"], input: ["src/**"] }`; `cache: true` is the same as `cache: {}`. `env`, `untrackedEnv`, `input`, and `output` are no longer supported at the top level of a task ([#749](https://github.com/voidzero-dev/vite-task/pull/749)). - **Fixed** Cached tasks on macOS no longer intermittently fail with exit 2 and `oils I/O error (main): No such process` when a fast command finishes before the shell gets scheduled. The bundled shell that runs task commands is updated to Oils 0.38.0, which fixes this race ([#702](https://github.com/voidzero-dev/vite-task/issues/702), [#703](https://github.com/voidzero-dev/vite-task/pull/703)). diff --git a/Cargo.lock b/Cargo.lock index d9496e796..867cf2389 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,6 +200,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -227,13 +233,19 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bindgen" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "cexpr", "clang-sys", "itertools 0.13.0", @@ -270,9 +282,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -490,6 +502,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -574,6 +613,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compact_str" version = "0.9.0" @@ -659,6 +708,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cow-utils" version = "0.1.3" @@ -732,7 +797,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "crossterm_winapi", "derive_more", "document-features", @@ -754,6 +819,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -1012,12 +1083,23 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "block2", "libc", "objc2", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "document-features" version = "0.2.12" @@ -1221,6 +1303,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1353,7 +1444,7 @@ name = "fspy_nostd" version = "0.0.0" dependencies = [ "atoi", - "bitflags 2.10.0", + "bitflags 2.13.2", "bstr", "libc", "linux-raw-sys 0.12.1", @@ -1430,7 +1521,7 @@ version = "0.0.0" dependencies = [ "allocator-api2", "assert2", - "bitflags 2.10.0", + "bitflags 2.13.2", "bstr", "bumpalo", "bytemuck", @@ -1456,7 +1547,7 @@ version = "0.0.0" dependencies = [ "allocator-api2", "anyhow", - "base64", + "base64 0.22.1", "bstr", "elf", "fspy_nostd", @@ -1649,6 +1740,17 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1702,6 +1804,45 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "hybrid-array" version = "0.4.11" @@ -1711,6 +1852,148 @@ dependencies = [ "typenum", ] +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec 1.15.1", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec 1.15.1", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -1723,6 +2006,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec 1.15.1", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indenter" version = "0.3.4" @@ -1756,7 +2060,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "inotify-sys", "libc", ] @@ -1798,6 +2102,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + [[package]] name = "is-terminal" version = "0.4.17" @@ -1845,6 +2155,55 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1955,7 +2314,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "libc", "redox_syscall 0.7.0", ] @@ -1989,7 +2348,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", ] [[package]] @@ -2016,6 +2375,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "litrs" version = "1.0.0" @@ -2113,6 +2478,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2169,7 +2550,7 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1d395473824516f38dd1071a1a37bc57daa7be65b293ebba4ead5f7abb017a2" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "ctor", "futures", "napi-build", @@ -2227,7 +2608,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -2239,7 +2620,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -2252,7 +2633,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -2281,7 +2662,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "fsevent-sys", "inotify", "kqueue", @@ -2299,7 +2680,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", ] [[package]] @@ -2504,6 +2885,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2640,6 +3027,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" version = "2.8.6" @@ -2845,6 +3238,15 @@ dependencies = [ "winreg", ] +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2952,7 +3354,7 @@ dependencies = [ name = "pty_terminal_test_client" version = "0.0.0" dependencies = [ - "base64", + "base64 0.22.1", "getrandom 0.4.2", "winapi", ] @@ -3042,7 +3444,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "compact_str", "hashbrown 0.16.1", "indoc", @@ -3094,7 +3496,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "hashbrown 0.16.1", "indoc", "instability", @@ -3133,7 +3535,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", ] [[package]] @@ -3142,7 +3544,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", ] [[package]] @@ -3206,25 +3608,77 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] -name = "rsqlite-vfs" -version = "0.1.0" +name = "reqwest" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.18", + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", ] [[package]] -name = "rusqlite" -version = "0.39.0" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "bitflags 2.10.0", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags 2.13.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", "libsqlite3-sys", "smallvec 1.15.1", "sqlite-wasm-rs", @@ -3266,13 +3720,86 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3294,6 +3821,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3308,6 +3844,29 @@ dependencies = [ "libc", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" @@ -3339,6 +3898,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -3512,6 +4081,22 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version 0.4.1", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -3594,6 +4179,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "stackalloc" version = "1.2.1" @@ -3650,7 +4241,7 @@ dependencies = [ name = "subprocess_test" version = "0.0.0" dependencies = [ - "base64", + "base64 0.22.1", "ctor", "fspy", "portable-pty", @@ -3658,6 +4249,12 @@ dependencies = [ "wincode", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "2.1.0" @@ -3699,6 +4296,37 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "syscalls" version = "0.8.1" @@ -3776,8 +4404,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", - "base64", - "bitflags 2.10.0", + "base64 0.22.1", + "bitflags 2.13.2", "fancy-regex", "filedescriptor", "finl_unicode", @@ -3903,6 +4531,16 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.49.0" @@ -3931,6 +4569,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -4015,6 +4663,51 @@ version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -4087,6 +4780,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "ts-rs" version = "12.0.1" @@ -4141,6 +4840,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.23" @@ -4182,6 +4887,24 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39abd59bf32521c7f2301b52d05a6a2c975b6003521cbd0c6dc1582f0a22104" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf8-chars" version = "3.0.6" @@ -4191,6 +4914,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -4276,6 +5005,7 @@ dependencies = [ "vt_ipc_shared", "vt_path", "vt_plan", + "vt_remote_cache", "vt_select", "vt_server", "vt_str", @@ -4482,6 +5212,23 @@ dependencies = [ "which", ] +[[package]] +name = "vt_remote_cache" +version = "0.0.0" +dependencies = [ + "ciborium", + "reqwest", + "rustls", + "serde", + "serde_bytes", + "tempfile", + "thiserror 2.0.18", + "tokio", + "url", + "vt_path", + "vt_str", +] + [[package]] name = "vt_select" version = "0.0.0" @@ -4603,6 +5350,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4640,6 +5396,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.108" @@ -4694,13 +5464,26 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "hashbrown 0.15.5", "indexmap", "semver 1.0.27", @@ -4721,6 +5504,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -4731,6 +5524,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "wezterm-bidi" version = "0.2.3" @@ -4883,13 +5685,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -4901,6 +5712,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -4908,58 +5735,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -5060,7 +5935,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.13.2", "indexmap", "log", "serde", @@ -5090,6 +5965,12 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + [[package]] name = "xattr" version = "1.6.1" @@ -5112,6 +5993,29 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.39" @@ -5132,6 +6036,66 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "zmij" version = "1.0.20" diff --git a/Cargo.toml b/Cargo.toml index 9a5647905..fb0603fc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ assert2 = "0.4.0" assertables = "10.0.0" async-trait = "0.1.89" base64 = "0.22.1" +url = "2.5.8" wincode = "0.6.0" bindgen = "0.72.1" bitflags = "2.10.0" @@ -56,6 +57,7 @@ bumpalo = "3.17.0" bytemuck = "1.23.0" cargo-platform = "=0.3.2" cc = "1.2.39" +ciborium = "0.2.2" clap = "4.5.53" color-eyre = "0.6.5" compact_str = "0.9.0" @@ -125,11 +127,14 @@ ratatui = "0.30.0" rayon = "1.10.0" ref-cast = "1.0.24" regex = "1.11.3" +reqwest = { version = "0.13.5", default-features = false } rusqlite = "0.39.0" rustc-hash = "2.1.1" +rustls = { version = "0.23.44", default-features = false } # SeccompAction::UserNotif (SECCOMP_RET_USER_NOTIF) was added after the latest published release (v0.5.0) seccompiler = { git = "https://github.com/rust-vmm/seccompiler", rev = "08587106340b8e3cb361c7561411510039436857" } serde = "1.0.219" +serde_bytes = "0.11.19" serde_json = "1.0.140" serde_norway = "0.9.42" sha2 = "0.11.0" @@ -166,6 +171,7 @@ vt_glob = { path = "crates/vt_glob" } vt_graph_ser = { path = "crates/vt_graph_ser" } vt_path = { path = "crates/vt_path" } vt_powershell = { path = "crates/vt_powershell" } +vt_remote_cache = { path = "crates/vt_remote_cache" } vt_select = { path = "crates/vt_select" } vt_shell = { path = "crates/vt_shell" } vt_str = { path = "crates/vt_str" } diff --git a/crates/vt/Cargo.toml b/crates/vt/Cargo.toml index 5705f55ea..e99b614e1 100644 --- a/crates/vt/Cargo.toml +++ b/crates/vt/Cargo.toml @@ -48,6 +48,7 @@ uuid = { workspace = true, features = ["v4"] } vt_casefold = { workspace = true } vt_glob = { workspace = true } vt_path = { workspace = true } +vt_remote_cache = { workspace = true } vt_select = { workspace = true } vt_str = { workspace = true } # Artifact dep under `[dependencies]` — not `[build-dependencies]`, where diff --git a/crates/vt/src/session/cache/mod.rs b/crates/vt/src/session/cache/mod.rs index 86148cd5b..b72530b25 100644 --- a/crates/vt/src/session/cache/mod.rs +++ b/crates/vt/src/session/cache/mod.rs @@ -2,6 +2,7 @@ pub mod archive; pub mod display; +pub mod remote; mod validation; use std::{collections::BTreeMap, fmt::Display, fs::File, io::Write, sync::Arc, time::Duration}; @@ -17,7 +18,10 @@ use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use vt_graph::config::ResolvedGlobConfig; use vt_path::{AbsolutePath, RelativePathBuf}; -use vt_plan::cache_metadata::{CacheMetadata, ExecutionCacheKey, SpawnFingerprint}; +use vt_plan::{ + cache_metadata::{CacheMetadata, ExecutionCacheKey, SpawnFingerprint}, + remote_cache::{RemoteCacheAccess, ResolvedRemoteCacheConfig}, +}; use vt_str::Str; use wincode::{ SchemaRead, SchemaReadOwned, SchemaWrite, @@ -26,6 +30,7 @@ use wincode::{ io::{Reader, Writer}, }; +use self::remote::{RemoteClients, UploadError}; use super::execute::{ fingerprint::{PostRunFingerprint, TrackedEnvQuery}, pipe::StdOutput, @@ -140,6 +145,7 @@ pub struct CacheEntryValue { #[derive(Debug)] pub struct ExecutionCache { conn: Mutex, + remote_clients: RemoteClients, } #[derive(Debug, Clone, Serialize)] @@ -310,7 +316,7 @@ impl ExecutionCache { CREATE TABLE IF NOT EXISTS task_fingerprints (key BLOB PRIMARY KEY, value BLOB);", )?; // Lock is released when lock_file is dropped - Ok(Self { conn: Mutex::new(conn) }) + Ok(Self { conn: Mutex::new(conn), remote_clients: RemoteClients::default() }) } #[tracing::instrument] @@ -364,13 +370,17 @@ impl ExecutionCache { /// If a previous entry exists for the same cache key with a different /// `output_archive`, the stale archive file in `cache_dir` is removed /// (best-effort) so it doesn't accumulate on disk. + /// + /// In `read-write` remote mode, the entry is then uploaded to the remote + /// cache. Returns `Ok(Err(_))` if the local update succeeded but the + /// upload failed. #[tracing::instrument(level = "debug", skip_all)] pub async fn update( &self, cache_metadata: &CacheMetadata, cache_value: CacheEntryValue, cache_dir: &AbsolutePath, - ) -> anyhow::Result<()> { + ) -> anyhow::Result> { let execution_cache_key = &cache_metadata.execution_cache_key; let cache_key = CacheEntryKey::from_metadata(cache_metadata); @@ -389,7 +399,20 @@ impl ExecutionCache { self.upsert_cache_entry(&cache_key, &cache_value).await?; self.upsert_task_fingerprint(execution_cache_key, &cache_key).await?; - Ok(()) + + let Some(ResolvedRemoteCacheConfig { access: RemoteCacheAccess::ReadWrite, url }) = + &cache_metadata.remote_cache + else { + return Ok(Ok(())); + }; + let upload = self + .remote_clients + .upload(url, &cache_key, execution_cache_key, &cache_value, cache_dir) + .await; + if let Err(err) = &upload { + tracing::debug!(?err, "remote cache upload failed"); + } + Ok(upload) } } diff --git a/crates/vt/src/session/cache/remote.rs b/crates/vt/src/session/cache/remote.rs new file mode 100644 index 000000000..922604253 --- /dev/null +++ b/crates/vt/src/session/cache/remote.rs @@ -0,0 +1,105 @@ +//! The remote cache tier. After a local update in `read-write` mode, the entry +//! is uploaded to the remote cache as opaque bytes: +//! +//! | Field | Contents | +//! | --------------- | ------------------------------------- | +//! | `key` | Header + wincode(`CacheEntryKey`) | +//! | `secondary_key` | Header + wincode(`ExecutionCacheKey`) | +//! | `value` | wincode(`CacheEntryValue`) | +//! | blob | The `.tar.zst` output archive | +//! +//! Both keys start with a header containing the cache schema version and the +//! target OS and architecture. Platforms share an endpoint's namespace, but +//! their keys differ. + +use std::sync::{Arc, Mutex, PoisonError}; + +use rustc_hash::FxHashMap; +use vt_path::AbsolutePath; +use vt_plan::cache_metadata::ExecutionCacheKey; +use vt_remote_cache::Client; +use vt_str::Str; +use wincode::{ + SchemaWrite, + error::{WriteError, WriteResult}, +}; + +use super::{ + CACHE_SCHEMA_VERSION, CacheEntryKey, CacheEntryValue, TaskCacheConfig, serialize_cache, +}; + +/// Why an entry wasn't uploaded. The reporter turns it into messages. +#[derive(Debug)] +pub enum UploadError { + Remote(vt_remote_cache::Error), + Encode(WriteError), +} + +impl From for UploadError { + fn from(err: vt_remote_cache::Error) -> Self { + Self::Remote(err) + } +} + +impl From for UploadError { + fn from(err: WriteError) -> Self { + Self::Encode(err) + } +} + +/// Remote cache clients, each created when its endpoint is first used. +#[derive(Debug, Default)] +pub struct RemoteClients { + clients: Mutex, Arc>>, +} + +impl RemoteClients { + fn client(&self, endpoint: &Arc) -> Result, vt_remote_cache::Error> { + let mut clients = self.clients.lock().unwrap_or_else(PoisonError::into_inner); + if let Some(client) = clients.get(endpoint) { + return Ok(Arc::clone(client)); + } + let client = Arc::new(Client::new(endpoint)?); + clients.insert(Arc::clone(endpoint), Arc::clone(&client)); + drop(clients); + Ok(client) + } + + /// Upload an entry that was just recorded locally, along with its output + /// archive in `cache_dir`. + pub(super) async fn upload( + &self, + endpoint: &Arc, + cache_key: &CacheEntryKey, + execution_cache_key: &ExecutionCacheKey, + cache_value: &CacheEntryValue, + cache_dir: &AbsolutePath, + ) -> Result<(), UploadError> { + let client = self.client(endpoint)?; + let key = encode_key(cache_key)?; + let secondary_key = encode_key(execution_cache_key)?; + let value = serialize_cache(cache_value)?; + let archive = cache_value.output_archive.as_ref().map(|name| cache_dir.join(name.as_str())); + client.store(&key, &secondary_key, &value, archive.as_deref()).await?; + Ok(()) + } +} + +#[derive(SchemaWrite)] +struct KeyHeader { + cache_schema_version: u32, + os: Str, + arch: Str, +} + +/// Encode a key with the header for this build. +fn encode_key>(key: &K) -> WriteResult> { + let header = KeyHeader { + cache_schema_version: CACHE_SCHEMA_VERSION, + os: Str::from(std::env::consts::OS), + arch: Str::from(std::env::consts::ARCH), + }; + let mut bytes = serialize_cache(&header)?; + bytes.extend(serialize_cache(key)?); + Ok(bytes) +} diff --git a/crates/vt/src/session/event.rs b/crates/vt/src/session/event.rs index b28b2f2d5..410f49a4b 100644 --- a/crates/vt/src/session/event.rs +++ b/crates/vt/src/session/event.rs @@ -3,7 +3,7 @@ use std::{process::ExitStatus, time::Duration}; use vt_path::RelativePathBuf; use vt_server::Error as IpcServerError; -use super::cache::CacheMiss; +use super::cache::{CacheMiss, remote::UploadError}; /// The cache operation that failed. #[derive(Debug)] @@ -110,7 +110,11 @@ pub enum CacheNotUpdatedReason { #[derive(Debug)] pub enum CacheUpdateStatus { /// Cache was successfully updated with new fingerprint and outputs - Updated, + Updated { + /// Why uploading the entry to the remote cache failed. `None` if the + /// upload succeeded or wasn't attempted. + upload_error: Option, + }, /// Cache was not updated (with reason). /// The reason is part of the `LeafExecutionReporter` trait contract — reporters /// can use it for detailed logging, even if current implementations don't. diff --git a/crates/vt/src/session/execute/cache_update.rs b/crates/vt/src/session/execute/cache_update.rs index 31160c4ba..029c48f52 100644 --- a/crates/vt/src/session/execute/cache_update.rs +++ b/crates/vt/src/session/execute/cache_update.rs @@ -186,7 +186,7 @@ pub(super) async fn update_cache( output_archive, }; match cache.update(metadata, new_cache_value, cache_dir).await { - Ok(()) => (CacheUpdateStatus::Updated, None), + Ok(upload) => (CacheUpdateStatus::Updated { upload_error: upload.err() }, None), Err(err) => ( CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::CacheDisabled), Some(ExecutionError::Cache { kind: CacheErrorKind::Update, source: err }), diff --git a/crates/vt/src/session/reporter/summary.rs b/crates/vt/src/session/reporter/summary.rs index 7bc36e5bb..e138eb326 100644 --- a/crates/vt/src/session/reporter/summary.rs +++ b/crates/vt/src/session/reporter/summary.rs @@ -19,6 +19,7 @@ use crate::session::{ cache::{ CacheMiss, EnvMismatch, FingerprintMismatch, InputChangeKind, SpawnFingerprintChange, detect_spawn_fingerprint_changes, format_input_change_str, format_spawn_change, + remote::UploadError, }, event::{ CacheDisabledReason, CacheErrorKind, CacheNotUpdatedReason, CacheStatus, CacheUpdateStatus, @@ -120,6 +121,9 @@ pub enum SpawnOutcome { /// Set when a runner-aware tool called `disableCache()`, skipping /// cache update. tool_disabled_cache: bool, + /// Why uploading the entry to the remote cache failed, if it did. + /// The local cache was still updated. + upload_error: Option, }, /// Process exited with non-zero status. @@ -170,6 +174,17 @@ pub enum SavedCacheErrorKind { Update, } +/// A failed remote cache operation, serializable for persistence. +/// +/// `reason` names only the kind of failure, so it's the same on every +/// platform. `details` has the underlying error, which only `--last-details` +/// shows. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SavedRemoteCacheError { + reason: Str, + details: Option, +} + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Computed stats (derived from tasks, not persisted) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -183,6 +198,13 @@ struct SummaryStats { total_saved: Duration, /// Display names of tasks that were not cached due to read-write overlap. input_modified_task_names: Vec, + /// Tasks whose upload to the remote cache failed. + upload_failures: Vec, +} + +struct UploadFailure { + task_name: Str, + reason: Str, } impl SummaryStats { @@ -195,6 +217,7 @@ impl SummaryStats { failed: 0, total_saved: Duration::ZERO, input_modified_task_names: Vec::new(), + upload_failures: Vec::new(), }; for task in tasks { @@ -220,6 +243,12 @@ impl SummaryStats { } SpawnOutcome::Success { .. } => {} } + if let SpawnOutcome::Success { upload_error: Some(error), .. } = outcome { + stats.upload_failures.push(UploadFailure { + task_name: task.format_task_display(), + reason: error.reason.clone(), + }); + } } } } @@ -288,6 +317,53 @@ impl SavedExecutionError { } } +impl SavedRemoteCacheError { + /// Convert a live [`UploadError`] into a serializable error. + fn from_upload_error(error: &UploadError) -> Self { + match error { + UploadError::Remote(error) => Self::from_client_error(error), + UploadError::Encode(error) => Self { + reason: Str::from("failed to encode the cache entry"), + details: Some(vt_str::format!("{error}")), + }, + } + } + + /// The client's message is the reason. The details come from the + /// underlying error, if any. + fn from_client_error(error: &vt_remote_cache::Error) -> Self { + use vt_remote_cache::Error; + + let details = match error { + Error::InvalidEndpoint(parse_error) => { + parse_error.as_ref().map(|parse_error| vt_str::format!("{parse_error}")) + } + // reqwest's own message only says which request failed. The cause, + // such as a refused connection, is in its sources. + Error::HttpClient(error) | Error::Network(error) => Some(error_chain(error)), + Error::ReadBlob(error) => Some(vt_str::format!("{error}")), + Error::Status(_, message) => { + message.as_ref().map(|message| vt_str::format!("{message}")) + } + }; + Self { reason: vt_str::format!("{error}"), details } + } + + /// Format the reason and details for display. + fn display_message(&self) -> Str { + self.details.as_ref().map_or_else( + || self.reason.clone(), + |details| vt_str::format!("{}: {details}", self.reason), + ) + } +} + +/// The messages of `error` and its sources, joined with `: `. +fn error_chain(error: &(dyn std::error::Error + 'static)) -> Str { + std::iter::successors(error.source(), |&source| source.source()) + .fold(vt_str::format!("{error}"), |chain, source| vt_str::format!("{chain}: {source}")) +} + impl SavedCacheMissReason { fn from_cache_miss(cache_miss: &CacheMiss) -> Self { match cache_miss { @@ -353,6 +429,12 @@ impl TaskResult { cache_update_status, CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::TrackingIncomplete) ); + let upload_error = match cache_update_status { + CacheUpdateStatus::Updated { upload_error: Some(err) } => { + Some(SavedRemoteCacheError::from_upload_error(err)) + } + _ => None, + }; match cache_status { CacheStatus::Hit { replayed_duration } => { @@ -369,6 +451,7 @@ impl TaskResult { ipc_server_error, tool_disabled_cache, tracking_incomplete, + upload_error, ), }, CacheStatus::Miss(cache_miss) => Self::Spawned { @@ -383,6 +466,7 @@ impl TaskResult { ipc_server_error, tool_disabled_cache, tracking_incomplete, + upload_error, ), }, } @@ -390,6 +474,10 @@ impl TaskResult { } /// Build a [`SpawnOutcome`] from process exit status and optional pre-converted error. +#[expect( + clippy::too_many_arguments, + reason = "each cache update detail is extracted by the caller and passed through" +)] fn spawn_outcome_from_execution( exit_status: Option, saved_error: Option<&SavedExecutionError>, @@ -398,6 +486,7 @@ fn spawn_outcome_from_execution( ipc_server_error: Option, tool_disabled_cache: bool, tracking_incomplete: bool, + upload_error: Option, ) -> SpawnOutcome { match (exit_status, saved_error) { // Spawn error — process never ran @@ -410,6 +499,7 @@ fn spawn_outcome_from_execution( ipc_server_error, tool_disabled_cache, tracking_incomplete, + upload_error, }, // Process exited with non-zero code (Some(status), _) => { @@ -431,6 +521,7 @@ fn spawn_outcome_from_execution( ipc_server_error: None, tool_disabled_cache: false, tracking_incomplete: false, + upload_error: None, }, } } @@ -641,6 +732,16 @@ impl TaskResult { } } + /// Why uploading the entry to the remote cache failed, if it did. + const fn upload_error(&self) -> Option<&SavedRemoteCacheError> { + match self { + Self::Spawned { outcome: SpawnOutcome::Success { upload_error, .. }, .. } => { + upload_error.as_ref() + } + _ => None, + } + } + /// Optional error associated with this result. pub const fn error(&self) -> Option<&SavedExecutionError> { match self { @@ -801,6 +902,15 @@ pub fn format_full_summary(summary: &LastRunSummary) -> Vec { let cache_detail = task.result.format_cache_detail(); let _ = writeln!(buf, " {}", cache_detail.style(task.result.cache_detail_style())); + if let Some(error) = task.result.upload_error() { + let _ = writeln!( + buf, + " {}", + vt_str::format!("⚠ Not uploaded to the remote cache: {}", error.display_message()) + .style(Style::new().yellow()) + ); + } + // Error message if present if let Some(err) = task.result.error() { let msg = err.display_message(); @@ -848,8 +958,12 @@ pub fn format_compact_summary(summary: &LastRunSummary, program_name: &str) -> V let is_single_task = summary.tasks.len() == 1; - // Single task + not cache hit + no input modification → no summary - if is_single_task && stats.cache_hits == 0 && stats.input_modified_task_names.is_empty() { + // Single task + not cache hit + no notice → no summary + if is_single_task + && stats.cache_hits == 0 + && stats.input_modified_task_names.is_empty() + && stats.upload_failures.is_empty() + { return Vec::new(); } @@ -908,14 +1022,17 @@ pub fn format_compact_summary(summary: &LastRunSummary, program_name: &str) -> V let _ = write!(buf, "."); } else { - // Single task, no cache hit — only shown when input_modified is non-empty + // Single task, no cache hit — only shown with a notice below let _ = write!(buf, "{}", run_label.as_str().style(Style::new().blue().bold())); } - // Inline input-modified notice before the --last-details hint + // Inline notices before the --last-details hint if !stats.input_modified_task_names.is_empty() { format_input_modified_notice(&mut buf, &stats.input_modified_task_names); } + if !stats.upload_failures.is_empty() { + format_upload_failed_notice(&mut buf, &stats.upload_failures); + } if show_last_details_hint { let last_details_cmd = vt_str::format!("`{program_name} run --last-details`"); @@ -946,10 +1063,81 @@ fn format_input_modified_notice(buf: &mut Vec, task_names: &[Str]) { } } +/// Write the "not uploaded to the remote cache" notice inline. The reason is +/// shown when all failed uploads share it. +fn format_upload_failed_notice(buf: &mut Vec, failures: &[UploadFailure]) { + let _ = write!(buf, " "); + + let first = &failures[0]; + let _ = write!(buf, "{}", first.task_name.as_str().style(Style::new().bold())); + let remaining = failures.len() - 1; + if remaining > 0 { + let _ = write!(buf, " (and {remaining} more)"); + } + + let _ = write!(buf, " not uploaded to the remote cache"); + if failures.iter().all(|failure| failure.reason == first.reason) { + let _ = write!(buf, ": {}", first.reason); + } + let _ = write!(buf, "."); +} + #[cfg(test)] mod tests { use super::*; + fn upload_failed_task(task_name: &str, reason: &str) -> TaskSummary { + TaskSummary { + package_name: Str::from("pkg"), + task_name: Str::from(task_name), + command: Str::from("build"), + cwd: Str::default(), + result: TaskResult::Spawned { + cache_status: SpawnedCacheStatus::Miss(SavedCacheMissReason::NotFound), + outcome: SpawnOutcome::Success { + infra_error: None, + input_modified_path: None, + fspy_unsupported: false, + ipc_server_error: None, + tracking_incomplete: false, + tool_disabled_cache: false, + upload_error: Some(SavedRemoteCacheError { + reason: Str::from(reason), + details: None, + }), + }, + }, + } + } + + fn compact_summary(tasks: Vec) -> Str { + let bytes = format_compact_summary(&LastRunSummary { tasks, exit_code: 0 }, "vp"); + vt_str::format!("{}", anstream::adapter::strip_str(std::str::from_utf8(&bytes).unwrap())) + } + + #[test] + fn upload_failure_notice_shows_a_shared_reason() { + let summary = compact_summary(vec![ + upload_failed_task("a", "network error"), + upload_failed_task("b", "network error"), + ]); + assert_eq!( + summary.as_str(), + "---\nvp run: 0/2 cache hit (0%). pkg#a (and 1 more) not uploaded to the remote cache: \ + network error. (Run `vp run --last-details` for full details)\n" + ); + + let summary = compact_summary(vec![ + upload_failed_task("a", "network error"), + upload_failed_task("b", "HTTP status 500"), + ]); + assert_eq!( + summary.as_str(), + "---\nvp run: 0/2 cache hit (0%). pkg#a (and 1 more) not uploaded to the remote cache. \ + (Run `vp run --last-details` for full details)\n" + ); + } + #[test] fn output_forwarding_error_has_distinct_message() { let error = ExecutionError::ForwardTaskProcessOutput(anyhow::anyhow!( diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/package.json new file mode 100644 index 000000000..f4afb1054 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/package.json @@ -0,0 +1,4 @@ +{ + "name": "remote-cache", + "private": true +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots.toml new file mode 100644 index 000000000..02e279e93 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots.toml @@ -0,0 +1,123 @@ +# Cases that use the remote cache backend are ignored because it runs on +# Node.js. Windows is skipped because the PTY launcher cannot execute pnpm +# command shims. +[[e2e]] +name = "read_write" +cfg = "not(windows)" +ignore = true +steps = [ + { argv = [ + "remote-cache-server", + "vt", + "run", + "build", + ], envs = [ + [ + "VP_REMOTE_CACHE", + "read-write", + ], + ], comment = "A new execution is uploaded with one store request." }, + { argv = [ + "remote-cache-server", + "vt", + "run", + "build", + ], envs = [ + [ + "VP_REMOTE_CACHE", + "read-write", + ], + ], comment = "A local hit makes no requests." }, +] + +[[e2e]] +name = "read" +cfg = "not(windows)" +ignore = true +steps = [ + { argv = [ + "remote-cache-server", + "vt", + "run", + "build", + ], envs = [ + [ + "VP_REMOTE_CACHE", + "read-write", + ], + ] }, + [ + "vtt", + "write-file", + "src/a.txt", + "changed", + ], + { argv = [ + "remote-cache-server", + "vt", + "run", + "build", + ], comment = "An endpoint without a mode selects read. The task reruns without uploading." }, +] + +[[e2e]] +name = "invalid_endpoint" +steps = [ + { argv = [ + "vt", + "run", + "build", + ], envs = [ + [ + "VP_REMOTE_CACHE", + "read-write", + ], + [ + "VP_REMOTE_CACHE_URL", + "cache.example/projects/test", + ], + ], comment = "The failed upload is a warning. The task succeeds." }, + { argv = [ + "vt", + "run", + "--last-details", + ], comment = "The details include the underlying error." }, + { argv = [ + "vt", + "run", + "build", + ], envs = [ + [ + "VP_REMOTE_CACHE", + "read-write", + ], + [ + "VP_REMOTE_CACHE_URL", + "cache.example/projects/test", + ], + ], comment = "The local cache was updated. Hits never upload." }, +] + +[[e2e]] +name = "unreachable_endpoint" +steps = [ + { argv = [ + "vt", + "run", + "build", + ], envs = [ + [ + "VP_REMOTE_CACHE", + "read-write", + ], + [ + "VP_REMOTE_CACHE_URL", + "http://127.0.0.1:0/projects/test", + ], + ], comment = "Nothing can listen on port 0. The failed upload is a warning. The task succeeds." }, + { argv = [ + "vt", + "run", + "--last-details", + ], comment = "The details include the underlying error." }, +] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/invalid_endpoint.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/invalid_endpoint.md new file mode 100644 index 000000000..9bbd3bc41 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/invalid_endpoint.md @@ -0,0 +1,44 @@ +# invalid_endpoint + +## `VP_REMOTE_CACHE=read-write VP_REMOTE_CACHE_URL=cache.example/projects/test vt run build` + +The failed upload is a warning. The task succeeds. + +``` +$ vtt write-file dist/output.txt built + +--- +vt run: remote-cache#build not uploaded to the remote cache: invalid endpoint. (Run `vt run --last-details` for full details) +``` + +## `vt run --last-details` + +The details include the underlying error. + +``` + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] remote-cache#build: $ vtt write-file dist/output.txt built ✓ + → Cache miss: no previous cache entry found + ⚠ Not uploaded to the remote cache: invalid endpoint: relative URL without a base +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +## `VP_REMOTE_CACHE=read-write VP_REMOTE_CACHE_URL=cache.example/projects/test vt run build` + +The local cache was updated. Hits never upload. + +``` +$ vtt write-file dist/output.txt built ◉ cache hit, replaying + +--- +vt run: cache hit. +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/read.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/read.md new file mode 100644 index 000000000..5ade066f1 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/read.md @@ -0,0 +1,22 @@ +# read + +## `VP_REMOTE_CACHE=read-write remote-cache-server vt run build` + +``` +$ vtt write-file dist/output.txt built + +[remote-cache] POST /store 200 +``` + +## `vtt write-file src/a.txt changed` + +``` +``` + +## `remote-cache-server vt run build` + +An endpoint without a mode selects read. The task reruns without uploading. + +``` +$ vtt write-file dist/output.txt built ○ cache miss: 'src/a.txt' modified, executing +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/read_write.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/read_write.md new file mode 100644 index 000000000..10fc112fd --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/read_write.md @@ -0,0 +1,22 @@ +# read_write + +## `VP_REMOTE_CACHE=read-write remote-cache-server vt run build` + +A new execution is uploaded with one store request. + +``` +$ vtt write-file dist/output.txt built + +[remote-cache] POST /store 200 +``` + +## `VP_REMOTE_CACHE=read-write remote-cache-server vt run build` + +A local hit makes no requests. + +``` +$ vtt write-file dist/output.txt built ◉ cache hit, replaying + +--- +vt run: cache hit. +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/unreachable_endpoint.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/unreachable_endpoint.md new file mode 100644 index 000000000..a83bed2c1 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/snapshots/unreachable_endpoint.md @@ -0,0 +1,33 @@ +# unreachable_endpoint + +## `VP_REMOTE_CACHE=read-write VP_REMOTE_CACHE_URL=http://127.0.0.1:0/projects/test vt run build` + +Nothing can listen on port 0. The failed upload is a warning. The task succeeds. + +``` +$ vtt write-file dist/output.txt built + +--- +vt run: remote-cache#build not uploaded to the remote cache: network error. (Run `vt run --last-details` for full details) +``` + +## `vt run --last-details` + +The details include the underlying error. + +``` + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] remote-cache#build: $ vtt write-file dist/output.txt built ✓ + → Cache miss: no previous cache entry found + ⚠ Not uploaded to the remote cache: network error: error sending request for url (http://127.0.0.1:0/projects/test/store): client error (Connect): tcp connect error: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/src/a.txt b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/src/a.txt new file mode 100644 index 000000000..789819226 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/src/a.txt @@ -0,0 +1 @@ +a diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/vite-task.json new file mode 100644 index 000000000..33a2ca322 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache/vite-task.json @@ -0,0 +1,11 @@ +{ + "tasks": { + "build": { + "command": "vtt write-file dist/output.txt built", + "cache": { + "input": ["src/**"], + "output": ["dist/**"] + } + } + } +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/blob_replacement.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/blob_replacement.md index f0004361e..ecf1cae0f 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/blob_replacement.md +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/blob_replacement.md @@ -4,6 +4,7 @@ ``` {"status": 404, "content_type": "text/plain; charset=utf-8", "body": "Blob not found"} +[remote-cache] GET /blob/missing 404 ``` ## `vtt write-file archive.txt 'first archive'` @@ -15,18 +16,21 @@ ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": "1"}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''A'\'', "secondary_key": '\''S'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "exact", "value": 'first', "blob_id": "1"}} +[remote-cache] POST /fetch 200 exact ``` ## `remote-cache-server cbor-http GET /blob/1` ``` {"status": 200, "content_type": "application/octet-stream", "body": 'first archive'} +[remote-cache] GET /blob/1 200 ``` ## `remote-cache-server cbor-http POST /store --form-data 'blob='\''second archive'\''' --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''S'\'', "value": '\''second'\''}'` @@ -35,18 +39,21 @@ Accept blob before metadata, with neither part supplying a filename. ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": "2"}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''A'\'', "secondary_key": '\''S'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "exact", "value": 'second', "blob_id": "2"}} +[remote-cache] POST /fetch 200 exact ``` ## `remote-cache-server cbor-http GET /blob/2` ``` {"status": 200, "content_type": "application/octet-stream", "body": 'second archive'} +[remote-cache] GET /blob/2 200 ``` ## `remote-cache-server cbor-http GET /blob/1` @@ -55,6 +62,7 @@ Previously returned IDs retain their original bytes. ``` {"status": 200, "content_type": "application/octet-stream", "body": 'first archive'} +[remote-cache] GET /blob/1 200 ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''S'\'', "value": '\''no archive'\''}'` @@ -63,24 +71,28 @@ Omitting blob clears the association. ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": null}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''A'\'', "secondary_key": '\''S'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "exact", "value": 'no archive', "blob_id": null}} +[remote-cache] POST /fetch 200 exact ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''S'\'', "value": '\''empty archive'\''}' --form-data blob=''` ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": "3"}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''A'\'', "secondary_key": '\''S'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "exact", "value": 'empty archive', "blob_id": "3"}} +[remote-cache] POST /fetch 200 exact ``` ## `remote-cache-server cbor-http GET /blob/3` @@ -89,4 +101,5 @@ An empty blob has an ID and downloads as an empty byte string. ``` {"status": 200, "content_type": "application/octet-stream", "body": ''} +[remote-cache] GET /blob/3 200 ``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/entries_and_associations.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/entries_and_associations.md index b03a65b48..5f9be001f 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/entries_and_associations.md +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/entries_and_associations.md @@ -4,24 +4,28 @@ ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "not_found"}} +[remote-cache] POST /fetch 200 not_found ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''S'\'', "value": '\''VA'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": null}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''U'\'', "value": '\''VA'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": null}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''B'\'', "secondary_key": '\''S'\'', "value": '\''VB'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": null}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''A'\'', "secondary_key": '\''S'\''}'` @@ -30,6 +34,7 @@ The exact entry survives reassignment of S to B. ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "exact", "value": 'VA', "blob_id": null}} +[remote-cache] POST /fetch 200 exact ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''C'\'', "secondary_key": '\''S'\''}'` @@ -38,18 +43,21 @@ The previous fetch did not change S, so fallback returns B's entry. ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "fallback", "key": 'B', "value": 'VB', "blob_id": null}} +[remote-cache] POST /fetch 200 fallback ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''T'\'', "value": '\''VA2'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": null}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''A'\'', "secondary_key": '\''missing'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "exact", "value": 'VA2', "blob_id": null}} +[remote-cache] POST /fetch 200 exact ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''C'\'', "secondary_key": '\''U'\''}'` @@ -58,16 +66,19 @@ An older association to A still resolves after A is replaced. ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "fallback", "key": 'A', "value": 'VA2', "blob_id": null}} +[remote-cache] POST /fetch 200 fallback ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''C'\'', "secondary_key": '\''T'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "fallback", "key": 'A', "value": 'VA2', "blob_id": null}} +[remote-cache] POST /fetch 200 fallback ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''C'\'', "secondary_key": '\''S'\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "fallback", "key": 'B', "value": 'VB', "blob_id": null}} +[remote-cache] POST /fetch 200 fallback ``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/invalid_requests.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/invalid_requests.md index 9d8e327d8..d294904b2 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/invalid_requests.md +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/invalid_requests.md @@ -4,96 +4,112 @@ ``` {"status": 404, "content_type": "text/plain; charset=utf-8", "body": "Route not found"} +[remote-cache] GET /missing 404 ``` ## `remote-cache-server cbor-http POST /fetch --data 'bad'` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Expected application/cbor"} +[remote-cache] POST /fetch 400 ``` ## `remote-cache-server cbor-http POST /fetch --data 'bad' --content-type application/cbor` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Expected byte strings: key, secondary_key"} +[remote-cache] POST /fetch 400 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": "text", "secondary_key": '\'''\''}'` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Expected byte strings: key, secondary_key"} +[remote-cache] POST /fetch 400 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\'''\''}'` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Expected byte strings: key, secondary_key"} +[remote-cache] POST /fetch 400 ``` ## `remote-cache-server cbor-http POST /fetch --cbor []` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Expected byte strings: key, secondary_key"} +[remote-cache] POST /fetch 400 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\'''\'', "key": '\''duplicate'\'', "secondary_key": '\'''\''}'` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Invalid CBOR"} +[remote-cache] POST /fetch 400 ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''S'\'', "value": '\''original'\''}' --form-data 'blob='\''original archive'\'''` ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": "1"}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /store --cbor {}` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Expected multipart/form-data"} +[remote-cache] POST /store 400 ``` ## `remote-cache-server cbor-http POST /store --form-data 'blob='\''missing metadata'\'''` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Missing metadata"} +[remote-cache] POST /store 400 ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''T'\'', "value": "wrong type"}'` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Expected byte strings: key, secondary_key, value"} +[remote-cache] POST /store 400 ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''T'\'', "value": '\''replacement'\''}' --form-cbor metadata={}` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Invalid multipart body"} +[remote-cache] POST /store 400 ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''A'\'', "secondary_key": '\''T'\'', "value": '\''replacement'\''}' --form-data blob='one' --form-data blob='two'` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Invalid multipart body"} +[remote-cache] POST /store 400 ``` ## `remote-cache-server cbor-http POST /store --form-cbor metadata={} --form-data unexpected='part'` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Invalid multipart body"} +[remote-cache] POST /store 400 ``` ## `remote-cache-server cbor-http POST /store --form-data 'metadata='\''wrong content type'\'''` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Invalid multipart body"} +[remote-cache] POST /store 400 ``` ## `remote-cache-server cbor-http POST /store --data ''\''--boundary\r\nContent-Disposition: form-data; name="metadata"\r\nContent-Type: application/cbor\r\n\r\n'\''' --content-type 'multipart/form-data; boundary=boundary'` ``` {"status": 400, "content_type": "text/plain; charset=utf-8", "body": "Invalid multipart body"} +[remote-cache] POST /store 400 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''A'\'', "secondary_key": '\''S'\''}'` @@ -102,12 +118,14 @@ Failed stores did not replace the value or blob. ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "exact", "value": 'original', "blob_id": "1"}} +[remote-cache] POST /fetch 200 exact ``` ## `remote-cache-server cbor-http GET /blob/1` ``` {"status": 200, "content_type": "application/octet-stream", "body": 'original archive'} +[remote-cache] GET /blob/1 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''B'\'', "secondary_key": '\''T'\''}'` @@ -116,4 +134,5 @@ Failed stores did not publish a secondary association. ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "not_found"}} +[remote-cache] POST /fetch 200 not_found ``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/opaque_binary_data.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/opaque_binary_data.md index ab3d40053..b9bb2cd5e 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/opaque_binary_data.md +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/opaque_binary_data.md @@ -4,34 +4,40 @@ ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": "1"}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": b64'\''AP+A'\'', "secondary_key": '\'''\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "exact", "value": b64'AP+A', "blob_id": "1"}} +[remote-cache] POST /fetch 200 exact ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\'''\'', "secondary_key": '\'''\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "fallback", "key": b64'AP+A', "value": b64'AP+A', "blob_id": "1"}} +[remote-cache] POST /fetch 200 fallback ``` ## `remote-cache-server cbor-http GET /blob/1` ``` {"status": 200, "content_type": "application/octet-stream", "body": b64'AP+A'} +[remote-cache] GET /blob/1 200 ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\'''\'', "secondary_key": '\'''\'', "value": '\'''\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": null}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\'''\'', "secondary_key": '\'''\''}'` ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "exact", "value": '', "blob_id": null}} +[remote-cache] POST /fetch 200 exact ``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/state_across_invocations.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/state_across_invocations.md index b73e06671..4eb0f38fd 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/state_across_invocations.md +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/remote_cache_backend/snapshots/state_across_invocations.md @@ -4,6 +4,7 @@ ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": "1"}} +[remote-cache] POST /store 200 ``` ## `remote-cache-server cbor-http POST /fetch --cbor '{"key": '\''C'\'', "secondary_key": '\''S'\''}'` @@ -12,6 +13,7 @@ A later invocation reads the stored entry and association. ``` {"status": 200, "content_type": "application/cbor", "body": {"kind": "fallback", "key": 'A', "value": 'VA', "blob_id": "1"}} +[remote-cache] POST /fetch 200 fallback ``` ## `remote-cache-server cbor-http POST /store --form-cbor 'metadata={"key": '\''B'\'', "secondary_key": '\''T'\'', "value": '\''VB'\''}' --form-data 'blob='\''second archive'\'''` @@ -20,6 +22,7 @@ Blob numbering continues across invocations. ``` {"status": 200, "content_type": "application/cbor", "body": {"blob_id": "2"}} +[remote-cache] POST /store 200 ``` ## `vtt write-file remote-cache/blobs/1 'replaced archive'` @@ -33,4 +36,5 @@ Each blob is a file named by its ID. ``` {"status": 200, "content_type": "application/octet-stream", "body": 'replaced archive'} +[remote-cache] GET /blob/1 200 ``` diff --git a/crates/vt_bin/tests/e2e_snapshots/redact.rs b/crates/vt_bin/tests/e2e_snapshots/redact.rs index 8d7469015..f0545987d 100644 --- a/crates/vt_bin/tests/e2e_snapshots/redact.rs +++ b/crates/vt_bin/tests/e2e_snapshots/redact.rs @@ -76,6 +76,11 @@ pub fn redact_e2e_output(mut output: String, workspace_root: &str) -> String { regex::Regex::new(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}").unwrap(); output = uuid_regex.replace_all(&output, "").into_owned(); + // Redact OS error messages like "Connection refused (os error 111)", which + // differ by platform, to "" + let os_error_regex = regex::Regex::new(r": [^:\n]* \(os error -?\d+\)").unwrap(); + output = os_error_regex.replace_all(&output, ": ").into_owned(); + // Redact durations like "0ns", "123ms" or "1.23s" to "" let duration_regex = regex::Regex::new(r"\d+(\.\d+)?(ns|ms|s)").unwrap(); output = duration_regex.replace_all(&output, "").into_owned(); diff --git a/crates/vt_remote_cache/Cargo.toml b/crates/vt_remote_cache/Cargo.toml new file mode 100644 index 000000000..4351ca4c7 --- /dev/null +++ b/crates/vt_remote_cache/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "vt_remote_cache" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[dependencies] +ciborium = { workspace = true } +reqwest = { workspace = true, features = ["multipart", "rustls-no-provider", "stream"] } +rustls = { workspace = true, features = ["ring", "std"] } +serde = { workspace = true, features = ["derive"] } +serde_bytes = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["fs"] } +url = { workspace = true } +vt_path = { workspace = true } +vt_str = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt"] } + +[lints] +workspace = true + +[lib] +doctest = false diff --git a/crates/vt_remote_cache/README.md b/crates/vt_remote_cache/README.md new file mode 100644 index 000000000..e54d4651e --- /dev/null +++ b/crates/vt_remote_cache/README.md @@ -0,0 +1,11 @@ +# vt_remote_cache + +Client for the [remote cache server API](https://github.com/voidzero-dev/vite-task/pull/713). It treats keys, values, and blobs as opaque bytes. Encoding task cache entries into them is up to the caller. + +`Client::new` takes the configured endpoint, which can include a namespace path, such as `https://cache.example.com/projects/my-project`. Each operation appends its route to that path, so a store goes to `https://cache.example.com/projects/my-project/store`. Endpoints that aren't HTTP or HTTPS URLs are rejected. + +`Client::store` sends a multipart request: a CBOR `metadata` part with the key, secondary key, and value as byte strings, and an optional `blob` part streamed from a file. Only HTTP 200 counts as success. The response body isn't decoded. + +reqwest configures TLS. It uses the process's default rustls crypto provider, which the client installs as ring unless one is already installed, and verifies certificates with the operating system's verifier. Connections time out after 10 seconds. Reads time out after 60 seconds, and until the response headers arrive, that limit also covers sending the request. + +`Error` names the kind of failure: an invalid endpoint, a client that couldn't be created, a blob file that couldn't be read, a network error (including timeouts), or a status other than 200. Its messages contain no OS-specific details, so they can be shown to users as is. The details are in the source: the underlying error, the parse error for an endpoint that isn't a URL, or the message in an error response's body. diff --git a/crates/vt_remote_cache/src/lib.rs b/crates/vt_remote_cache/src/lib.rs new file mode 100644 index 000000000..f7396ed74 --- /dev/null +++ b/crates/vt_remote_cache/src/lib.rs @@ -0,0 +1,313 @@ +//! Client for the remote cache server API. Keys, values, and blobs are opaque +//! bytes; the caller decides what they contain. + +use std::time::Duration; + +use reqwest::{ + Response, StatusCode, + multipart::{Form, Part}, +}; +use serde::Serialize; +use url::{ParseError, Url}; +use vt_path::AbsolutePath; +use vt_str::Str; + +/// Time allowed to establish a connection, including the TLS handshake. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +/// Time allowed for each read of a response. Until the response headers +/// arrive, it also bounds sending the request. +const READ_TIMEOUT: Duration = Duration::from_secs(60); + +/// A failed remote cache operation. The messages name only the kind of +/// failure, so they are the same on every platform. The details, if any, are +/// in the source. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The endpoint isn't an HTTP or HTTPS URL that can have a path. The + /// source is the parse error if it isn't a URL at all. + #[error("invalid endpoint")] + InvalidEndpoint(#[source] Option), + /// The HTTP client couldn't be created, for example because no root + /// certificates could be loaded. + #[error("failed to create the HTTP client")] + HttpClient(#[source] reqwest::Error), + /// The blob file couldn't be opened. + #[error("failed to read the blob")] + ReadBlob(#[source] std::io::Error), + /// No complete response arrived, for example because the connection + /// failed or timed out. + #[error("network error")] + Network(#[source] reqwest::Error), + /// The server responded with a status other than 200. The source is the + /// message in the response body, if any. + #[error("HTTP status {}", .0.as_u16())] + Status(StatusCode, #[source] Option), +} + +/// The message in the body of an error response. +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +pub struct ServerMessage(Str); + +/// The `metadata` part of a store request. +#[derive(Serialize)] +struct StoreMetadata<'a> { + #[serde(with = "serde_bytes")] + key: &'a [u8], + #[serde(with = "serde_bytes")] + secondary_key: &'a [u8], + #[serde(with = "serde_bytes")] + value: &'a [u8], +} + +/// A client for one remote cache endpoint. +#[derive(Debug)] +pub struct Client { + http: reqwest::Client, + store_url: Url, +} + +impl Client { + /// Create a client for `endpoint`, a base URL that may include a + /// namespace path, such as `https://cache.example.com/projects/my-project`. + /// + /// # Errors + /// + /// Returns [`Error::InvalidEndpoint`] if `endpoint` isn't a usable URL, or + /// [`Error::HttpClient`] if the HTTP client can't be created. + pub fn new(endpoint: &str) -> Result { + let endpoint = parse_endpoint(endpoint)?; + let store_url = route_url(&endpoint, "store")?; + // reqwest configures TLS with the process's default crypto provider. + // Installing fails if one is already installed; vite-plus installs + // ring too. + let _ = rustls::crypto::ring::default_provider().install_default(); + let http = reqwest::Client::builder() + .connect_timeout(CONNECT_TIMEOUT) + .read_timeout(READ_TIMEOUT) + .build() + .map_err(Error::HttpClient)?; + Ok(Self { http, store_url }) + } + + /// Store `value` under `key` with `POST {endpoint}/store`, uploading the + /// file at `blob` as its blob. Fetches that match no key fall back to this + /// entry through `secondary_key`. + /// + /// # Errors + /// + /// Returns an error if the blob file can't be opened, the request fails, + /// or the server responds with a status other than 200. + pub async fn store( + &self, + key: &[u8], + secondary_key: &[u8], + value: &[u8], + blob: Option<&AbsolutePath>, + ) -> Result<(), Error> { + let metadata = StoreMetadata { key, secondary_key, value }; + let mut form = Form::new().part("metadata", metadata_part(&metadata)); + if let Some(blob) = blob { + form = form.part("blob", blob_part(blob).await?); + } + let response = self + .http + .post(self.store_url.clone()) + .multipart(form) + .send() + .await + .map_err(Error::Network)?; + if response.status() != StatusCode::OK { + return Err(status_error(response).await); + } + // The response's blob ID isn't needed. Read the body anyway, so the + // connection can be reused. + response.bytes().await.map_err(Error::Network)?; + Ok(()) + } +} + +fn parse_endpoint(endpoint: &str) -> Result { + let url = Url::parse(endpoint).map_err(|err| Error::InvalidEndpoint(Some(err)))?; + if !matches!(url.scheme(), "http" | "https") { + return Err(Error::InvalidEndpoint(None)); + } + Ok(url) +} + +/// Append `route` to the endpoint's path, keeping its namespace path. +fn route_url(endpoint: &Url, route: &str) -> Result { + let mut url = endpoint.clone(); + url.path_segments_mut().map_err(|()| Error::InvalidEndpoint(None))?.pop_if_empty().push(route); + Ok(url) +} + +/// The error for a response with a status other than 200, with the message in +/// its body. +async fn status_error(response: Response) -> Error { + let status = response.status(); + let message = response.text().await.ok().and_then(|text| { + let text = text.trim(); + (!text.is_empty()).then(|| ServerMessage(Str::from(text))) + }); + Error::Status(status, message) +} + +fn encode_metadata(metadata: &StoreMetadata<'_>) -> Vec { + let mut bytes = Vec::new(); + ciborium::into_writer(metadata, &mut bytes) + .expect("encoding byte strings into a Vec can't fail"); + bytes +} + +fn metadata_part(metadata: &StoreMetadata<'_>) -> Part { + Part::bytes(encode_metadata(metadata)).mime_str("application/cbor").expect("valid MIME type") +} + +async fn blob_part(path: &AbsolutePath) -> Result { + let file = tokio::fs::File::open(path).await.map_err(Error::ReadBlob)?; + let length = file.metadata().await.map_err(Error::ReadBlob)?.len(); + Ok(Part::stream_with_length(file, length) + .mime_str("application/octet-stream") + .expect("valid MIME type")) +} + +#[cfg(test)] +mod tests { + use std::{ + io::{Read as _, Write as _}, + net::TcpListener, + }; + + use vt_path::AbsolutePathBuf; + + use super::*; + + fn store_url(endpoint: &str) -> Result { + route_url(&parse_endpoint(endpoint)?, "store") + } + + #[test] + fn store_url_keeps_the_namespace_path() { + for (endpoint, expected) in [ + ("http://cache.example/projects/test", "http://cache.example/projects/test/store"), + ("http://cache.example/projects/test/", "http://cache.example/projects/test/store"), + ("https://cache.example", "https://cache.example/store"), + ("https://cache.example/ns?token=a", "https://cache.example/ns/store?token=a"), + ] { + assert_eq!(store_url(endpoint).unwrap().as_str(), expected, "{endpoint}"); + } + } + + #[test] + fn rejects_endpoints_that_are_not_http_urls() { + for endpoint in ["cache.example/projects/test", "ftp://cache.example", "mailto:a@b.example"] + { + assert!(matches!(Client::new(endpoint), Err(Error::InvalidEndpoint(_))), "{endpoint}"); + } + } + + #[test] + fn metadata_is_a_cbor_map_of_byte_strings() { + let metadata = StoreMetadata { key: b"k", secondary_key: b"", value: &[0x00, 0xff] }; + let mut expected = vec![0xa3]; + expected.extend(b"\x63key\x41k"); + expected.extend(b"\x6dsecondary_key\x40"); + expected.extend(b"\x65value\x42\x00\xff"); + assert_eq!(encode_metadata(&metadata), expected); + } + + fn contains(haystack: &[u8], needle: &[u8]) -> bool { + haystack.windows(needle.len()).any(|window| window == needle) + } + + /// Accept one HTTP request, respond with `status_line` and `body`, and + /// return the raw request. + fn serve_once(listener: &TcpListener, status_line: &str, body: &[u8]) -> Vec { + let (mut stream, _) = listener.accept().unwrap(); + let mut request = Vec::new(); + let mut buf = [0; 4096]; + let header_end = loop { + let n = stream.read(&mut buf).unwrap(); + assert_ne!(n, 0, "connection closed before the request headers ended"); + request.extend_from_slice(&buf[..n]); + if let Some(pos) = request.windows(4).position(|window| window == b"\r\n\r\n") { + break pos + 4; + } + }; + let content_length: usize = std::str::from_utf8(&request[..header_end]) + .unwrap() + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + name.eq_ignore_ascii_case("content-length").then(|| value.trim().parse().unwrap()) + }) + .expect("request has a content length"); + while request.len() < header_end + content_length { + let n = stream.read(&mut buf).unwrap(); + assert_ne!(n, 0, "connection closed before the request body ended"); + request.extend_from_slice(&buf[..n]); + } + let headers = vt_str::format!("{status_line}\r\ncontent-length: {}\r\n\r\n", body.len()); + stream.write_all(&[headers.as_bytes(), body].concat()).unwrap(); + request + } + + /// The `metadata` part of a store request for key `k`, secondary key `s`, + /// and value `v`. + fn metadata_part_bytes() -> Vec { + let metadata = StoreMetadata { key: b"k", secondary_key: b"s", value: b"v" }; + [ + b"name=\"metadata\"\r\nContent-Type: application/cbor\r\n\r\n".as_slice(), + &encode_metadata(&metadata), + b"\r\n", + ] + .concat() + } + + #[tokio::test] + async fn store_posts_metadata_and_blob_parts() { + let dir = tempfile::tempdir().unwrap(); + let blob = AbsolutePathBuf::new(dir.path().join("archive.tar.zst")).unwrap(); + std::fs::write(blob.as_path(), b"archive bytes").unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = std::thread::spawn(move || { + serve_once(&listener, "HTTP/1.1 500 Internal Server Error", b"storage failed\n") + }); + + let client = + Client::new(&vt_str::format!("http://127.0.0.1:{port}/projects/test")).unwrap(); + let err = client.store(b"k", b"s", b"v", Some(&blob)).await.unwrap_err(); + assert!(matches!(err, Error::Status(StatusCode::INTERNAL_SERVER_ERROR, _))); + assert_eq!(err.to_string(), "HTTP status 500"); + assert_eq!(std::error::Error::source(&err).unwrap().to_string(), "storage failed"); + + let request = server.join().unwrap(); + assert!(request.starts_with(b"POST /projects/test/store HTTP/1.1\r\n")); + assert!(contains(&request, b"content-type: multipart/form-data; boundary=")); + assert!(contains(&request, &metadata_part_bytes())); + let blob = + b"name=\"blob\"\r\nContent-Type: application/octet-stream\r\n\r\narchive bytes\r\n"; + assert!(contains(&request, blob)); + } + + #[tokio::test] + async fn store_without_a_blob_sends_only_metadata() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + // `0xff` can't start a CBOR item, so this body doesn't decode. + let server = + std::thread::spawn(move || serve_once(&listener, "HTTP/1.1 200 OK", b"\xffnot cbor")); + + let client = + Client::new(&vt_str::format!("http://127.0.0.1:{port}/projects/test")).unwrap(); + client.store(b"k", b"s", b"v", None).await.unwrap(); + + let request = server.join().unwrap(); + assert!(request.starts_with(b"POST /projects/test/store HTTP/1.1\r\n")); + assert!(contains(&request, &metadata_part_bytes())); + assert!(!contains(&request, b"name=\"blob\"")); + } +} diff --git a/deny.toml b/deny.toml index 4644e0a61..bf028f611 100644 --- a/deny.toml +++ b/deny.toml @@ -70,6 +70,8 @@ exceptions = [ # Each entry is the crate and version constraint, and its specific allow # list # { allow = ["Zlib"], name = "adler32", version = "*" }, + # Mozilla's root certificates, used by rustls-platform-verifier on wasm32 only. + { allow = ["CDLA-Permissive-2.0"], name = "webpki-root-certs" }, ] # Some crates don't have (easily) machine readable licensing information, diff --git a/packages/tools/README.md b/packages/tools/README.md index d31b82665..f5489c440 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -11,6 +11,14 @@ remote-cache-server cbor-http POST /fetch --cbor "{\"key\": 'A', \"secondary_key `remote-cache-server COMMAND [ARGS...]` starts the backend on a free loopback port and runs the command with `VP_REMOTE_CACHE_URL` set to the endpoint, `http://127.0.0.1:/projects/test`. The fixed base path gives every endpoint a namespace path. The wrapper takes no options and passes all arguments to the command unchanged. The command inherits stdio. When it exits, the server stops and the wrapper exits with the command's exit code. +After the command exits, the wrapper prints one line to stderr for each request it served, in the order of the responses. Each line has the method, the path below the base path, and the status. Fetch responses add their kind: + +```text +[remote-cache] POST /fetch 200 not_found +[remote-cache] POST /store 200 +[remote-cache] GET /blob/1 200 +``` + State persists in `remote-cache/` in the current directory, so consecutive commands share it. Each E2E case has its own directory and state. `state.json` holds the entries, associations, and next blob ID, with keys and values hex-encoded. Each blob is a file in `remote-cache/blobs/` named by its blob ID. Blob IDs are sequential strings and continue across invocations, keeping snapshots deterministic. The backend implements `POST /fetch`, `POST /store`, and `GET /blob/{blob_id}` from the [remote cache server API](https://github.com/voidzero-dev/vite-task/pull/713). Keys, values, and blobs are opaque bytes without length limits. There is no authentication. diff --git a/packages/tools/src/remote-cache/cli.ts b/packages/tools/src/remote-cache/cli.ts index fa4125ee8..b118e8b18 100755 --- a/packages/tools/src/remote-cache/cli.ts +++ b/packages/tools/src/remote-cache/cli.ts @@ -7,7 +7,12 @@ import { createCacheServer } from './server.ts'; const basePath = '/projects/test'; const [command, ...args] = process.argv.slice(2); -const server = createCacheServer({ basePath, directory: 'remote-cache' }); +const requests: string[] = []; +const server = createCacheServer({ + basePath, + directory: 'remote-cache', + logRequest: (line) => requests.push(line), +}); server.listen(0, '127.0.0.1'); await once(server, 'listening'); const { port } = server.address() as AddressInfo; @@ -17,4 +22,5 @@ const child = spawn(command!, args, { }); const [code] = (await once(child, 'exit')) as [number | null]; server.close(); +for (const line of requests) console.error(`[remote-cache] ${line}`); process.exitCode = code; diff --git a/packages/tools/src/remote-cache/server.ts b/packages/tools/src/remote-cache/server.ts index f4c7a551b..88a1b6f14 100644 --- a/packages/tools/src/remote-cache/server.ts +++ b/packages/tools/src/remote-cache/server.ts @@ -97,14 +97,18 @@ function cbor(response: ServerResponse, value: unknown): void { /** * A test backend that keeps its state in `directory`: entries and associations * in `state.json`, and each blob in `blobs/` under its ID. Keys, values, and - * blobs remain opaque bytes. + * blobs remain opaque bytes. After each response, `logRequest` receives a line + * with the method, the route below `basePath`, the status, and for fetch + * responses, the kind. */ export function createCacheServer({ basePath, directory, + logRequest, }: { basePath: string; directory: string; + logRequest: (line: string) => void; }) { const stateFile = join(directory, 'state.json'); const blobDirectory = join(directory, 'blobs'); @@ -115,14 +119,18 @@ export function createCacheServer({ const associations = new Map(Object.entries(state.associations)); let nextBlobId = state.next_blob_id; - async function handle(request: IncomingMessage, response: ServerResponse): Promise { - const path = new URL(request.url ?? '/', 'http://localhost').pathname; + /** Respond to `request`, returning the kind of a fetch response. */ + async function handle( + request: IncomingMessage, + response: ServerResponse, + path: string, + ): Promise { if (request.method === 'GET' && path.startsWith(`${basePath}/blob/`)) { const file = join(blobDirectory, path.slice(`${basePath}/blob/`.length)); if (!existsSync(file)) throw new RequestError(404, 'Blob not found'); response.writeHead(200, { 'content-type': 'application/octet-stream' }); response.end(readFileSync(file)); - return; + return undefined; } if (request.method !== 'POST' || ![`${basePath}/fetch`, `${basePath}/store`].includes(path)) { throw new RequestError(404, 'Route not found'); @@ -140,17 +148,19 @@ export function createCacheServer({ const fallback = associatedKey === undefined ? undefined : entries.get(associatedKey); if (exact) { cbor(response, { kind: 'exact', value: fromHex(exact.value), blob_id: exact.blob_id }); - } else if (fallback) { + return 'exact'; + } + if (fallback) { cbor(response, { kind: 'fallback', key: fromHex(associatedKey!), value: fromHex(fallback.value), blob_id: fallback.blob_id, }); - } else { - cbor(response, { kind: 'not_found' }); + return 'fallback'; } - return; + cbor(response, { kind: 'not_found' }); + return 'not_found'; } if (mediaType(contentType) !== 'multipart/form-data') { @@ -179,10 +189,16 @@ export function createCacheServer({ }; writeFileSync(stateFile, `${JSON.stringify(saved, null, 2)}\n`); cbor(response, { blob_id: blobId }); + return undefined; } return createServer((request, response) => { - void handle(request, response).catch((error: unknown) => { + const path = new URL(request.url ?? '/', 'http://localhost').pathname; + const log = (kind?: string) => { + const parts = [request.method, path.slice(basePath.length), response.statusCode, kind]; + logRequest(parts.filter((part) => part !== undefined).join(' ')); + }; + void handle(request, response, path).then(log, (error: unknown) => { const known = error instanceof RequestError; if (!known) console.error(error); response.writeHead(known ? error.status : 500, { @@ -190,6 +206,7 @@ export function createCacheServer({ }); response.end(known ? error.message : 'Internal server error'); request.resume(); + log(); }); }); }