From 279a7e05219c596804c037c6c40f165bce04eb89 Mon Sep 17 00:00:00 2001 From: SFG545 Date: Wed, 8 Jul 2026 16:35:09 -0500 Subject: [PATCH] Codex refactor update transactions Codex did this refactor. Batch update installs, dependency installs, conflict removals, and replacement removals under one update transaction hook boundary, following pacman's sync transaction model. Remove the unused pdf-extract dependency and refresh related dependency versions. --- Cargo.lock | 345 +----------------- Cargo.toml | 15 +- src/commands.rs | 575 +++++++++++++++++++++++------- src/commands/tests.rs | 125 +++++++ src/commands/update/candidates.rs | 72 ++-- 5 files changed, 615 insertions(+), 517 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16cc782..5d4f7b3 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,26 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "adobe-cmap-parser" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3" -dependencies = [ - "pom", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", -] - [[package]] name = "aes" version = "0.9.1" @@ -109,9 +89,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "ar" @@ -198,27 +178,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - [[package]] name = "bytes" version = "1.11.1" @@ -234,15 +199,6 @@ dependencies = [ "libbz2-rs-sys", ] -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "cc" version = "1.2.63" @@ -255,12 +211,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cff-parser" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f5b6e9141c036f3ff4ce7b2f7e432b0f00dee416ddcd4f17741d189ddc2e9d" - [[package]] name = "cfg-if" version = "1.0.4" @@ -328,9 +278,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.5" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" +checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b" dependencies = [ "clap", ] @@ -376,9 +326,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -531,7 +481,7 @@ checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" [[package]] name = "depot" -version = "0.50.0" +version = "0.60.0" dependencies = [ "anyhow", "ar", @@ -550,7 +500,6 @@ dependencies = [ "md5", "minisign", "nix", - "pdf-extract", "petgraph", "reqwest", "rusqlite", @@ -653,15 +602,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ecb" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "encode_unicode" version = "1.0.0" @@ -693,15 +633,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "euclid" -version = "0.20.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad" -dependencies = [ - "num-traits", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -832,19 +763,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-sink" version = "0.3.32" @@ -1260,9 +1178,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.4" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "portable-atomic", @@ -1277,7 +1195,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding", "generic-array", ] @@ -1469,34 +1386,6 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" -[[package]] -name = "lopdf" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7184fdea2bc3cd272a1acec4030c321a8f9875e877b3f92a53f2f6033fdc289" -dependencies = [ - "aes 0.8.4", - "bitflags", - "cbc", - "ecb", - "encoding_rs", - "flate2", - "getrandom 0.3.4", - "indexmap", - "itoa", - "log", - "md-5", - "nom", - "nom_locate", - "rand", - "rangemap", - "sha2 0.10.9", - "stringprep", - "thiserror 2.0.18", - "ttf-parser", - "weezl", -] - [[package]] name = "lz4_flex" version = "0.13.1" @@ -1508,9 +1397,9 @@ dependencies = [ [[package]] name = "lzma-rust2" -version = "0.16.4" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce716bf1a316f47a280fc76295f6495b5bea4752bca01c3b3885e101b1c23c02" +checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" dependencies = [ "sha2 0.11.0", ] @@ -1526,16 +1415,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest 0.10.7", -] - [[package]] name = "md5" version = "0.8.0" @@ -1617,26 +1496,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "nom_locate" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" -dependencies = [ - "bytecount", - "memchr", - "nom", -] - [[package]] name = "num-conv" version = "0.2.2" @@ -1717,12 +1576,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -1766,23 +1619,6 @@ dependencies = [ "hmac 0.13.0", ] -[[package]] -name = "pdf-extract" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28ba1758a3d3f361459645780e09570b573fc3c82637449e9963174c813a98" -dependencies = [ - "adobe-cmap-parser", - "cff-parser", - "encoding_rs", - "euclid", - "log", - "lopdf", - "postscript", - "type1-encoding-parser", - "unicode-normalization", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1813,24 +1649,12 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "pom" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6" - [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" -[[package]] -name = "postscript" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306" - [[package]] name = "potential_utf" version = "0.1.5" @@ -1852,15 +1676,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -1901,41 +1716,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rangemap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" - [[package]] name = "redox_syscall" version = "0.5.18" @@ -2364,17 +2144,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" @@ -2389,12 +2158,11 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "suppaftp" -version = "8.0.4" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf00e4d8418c477a8cb3c13ae5396a68d31658e760c74280bdbd34926e3b94b" +checksum = "81b8286a5639930b8fc0400f8a5111439ffb57e5fba6e8276a8bce857ec47b18" dependencies = [ "chrono", - "futures-lite", "lazy-regex", "log", "thiserror 2.0.18", @@ -2519,9 +2287,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.49" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "js-sys", @@ -2540,9 +2308,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.29" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -2558,21 +2326,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.52.3" @@ -2718,27 +2471,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - [[package]] name = "twox-hash" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" -[[package]] -name = "type1-encoding-parser" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa10c302f5a53b7ad27fd42a3996e23d096ba39b5b8dd6d9e683a05b01bee749" -dependencies = [ - "pom", -] - [[package]] name = "typed-path" version = "0.12.3" @@ -2751,33 +2489,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -2990,12 +2707,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - [[package]] name = "winapi" version = "0.3.9" @@ -3327,26 +3038,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.8" @@ -3413,7 +3104,7 @@ version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ - "aes 0.9.1", + "aes", "bzip2", "constant_time_eq 0.4.2", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 234afb7..a049978 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "depot" -version = "0.50.0" +version = "0.60.0" edition = "2024" [lints.rust] @@ -19,13 +19,13 @@ static-except-libc = [ ] [dependencies] -anyhow = "1.0.102" +anyhow = "1.0.103" ar = "0.9.0" bzip2 = "0.6.1" clap = { version = "4.6.0", features = ["derive"] } flate2 = "1.1.9" git2 = "0.21.0" -indicatif = "0.18.4" +indicatif = "0.18.6" nix = { version = "0.31.3", features = ["user"] } rusqlite = "0.40.1" semver = "1.0.28" @@ -43,22 +43,21 @@ zip = "8.5.0" zstd = { version = "0.13.3", features = ["zstdmt"] } inquire = "0.9.4" md5 = "0.8.0" -suppaftp = "8.0.4" +suppaftp = "10.0.0" minisign = "0.9.1" petgraph = "0.8.3" fd-lock = "4.0.4" reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "native-tls"] } filetime = "0.2.29" -clap_complete = "4.6.5" +clap_complete = "4.6.7" sys-mount = { version = "3.1.0", default-features = false } -time = { version = "0.3.49", features = ["formatting", "parsing"] } +time = { version = "0.3.53", features = ["formatting", "parsing"] } b2sum-rust = "0.3.0" serde_ignored = "0.1.14" lz4_flex = "0.13.1" -lzma-rust2 = "0.16.4" +lzma-rust2 = "0.16.5" signal-hook = "0.4.4" sha1 = "0.11.0" -pdf-extract = "0.10.0" [dev-dependencies] tempfile = "=3.27.0" diff --git a/src/commands.rs b/src/commands.rs index 29b2f31..da7d4e8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -735,6 +735,30 @@ struct PlannedPackageInstall { staged: PlannedStagedInstall, } +#[derive(Debug, Clone)] +struct PlannedInstalledRemoval { + package: String, + affected_paths: Vec, +} + +struct PreparedDirectInstallResources<'a> { + _staging_dir: Option, + _source_cleanup_guard: SourceBuildCleanupGuard<'a>, +} + +struct PreparedDirectInstall<'a> { + plans: Vec, + resources: PreparedDirectInstallResources<'a>, +} + +struct DirectInstallPreparationOptions<'a> { + build_dir: &'a Path, + clean_sources_before_build: bool, + suppress_output: bool, + confirm_installation: bool, + resolve_installed_conflicts: bool, +} + #[derive(Clone, Copy)] struct PendingLifecycleHook { hook: install::scripts::Hook, @@ -946,6 +970,26 @@ pub(crate) fn remove_installed_package_with_hooks( affected_paths: &affected_paths, }, )?; + remove_installed_package_without_transaction_hooks(package, rootfs, config, &affected_paths)?; + install::hooks::run_transaction_hooks( + rootfs, + &install::hooks::HookExecutionContext { + phase: install::hooks::HookPhase::Post, + operation: install::hooks::HookOperation::Remove, + package, + affected_paths: &affected_paths, + }, + )?; + Ok(()) +} + +fn remove_installed_package_without_transaction_hooks( + package: &str, + rootfs: &Path, + config: &config::Config, + _affected_paths: &[String], +) -> Result<()> { + let db_path = config.installed_db_path(rootfs); let script_dir = install::scripts::installed_scripts_dir(rootfs, package); let _ = install::scripts::run_hook_if_present( &script_dir, @@ -963,34 +1007,25 @@ pub(crate) fn remove_installed_package_with_hooks( let cleanup_scripts = install::scripts::remove_installed_scripts(rootfs, package); post_remove?; cleanup_scripts?; - install::hooks::run_transaction_hooks( - rootfs, - &install::hooks::HookExecutionContext { - phase: install::hooks::HookPhase::Post, - operation: install::hooks::HookOperation::Remove, - package, - affected_paths: &affected_paths, - }, - )?; ui::success(format!("Successfully removed {}", package)); Ok(()) } -fn resolve_installed_conflicts_for_subjects( +fn prompt_installed_conflict_removals_for_subjects( subjects: &[InstallConflictSubject], rootfs: &Path, config: &config::Config, dry_run: bool, -) -> Result<()> { +) -> Result> { if subjects.is_empty() { - return Ok(()); + return Ok(Vec::new()); } let db_path = config.installed_db_path(rootfs); let installed = collect_installed_conflict_packages(&db_path)?; let removals = collect_conflicting_installed_packages(subjects, &installed)?; if removals.is_empty() { - return Ok(()); + return Ok(Vec::new()); } let prompt_entries: Vec = removals @@ -1009,16 +1044,27 @@ fn resolve_installed_conflicts_for_subjects( "Dry run: would remove conflicting installed package(s): {}", prompt_entries.join(", ") )); - return Ok(()); + return Ok(Vec::new()); } if !ui::prompt_package_action("conflict removal", &prompt_entries, true)? { anyhow::bail!("Aborted"); } - for package in removals.keys() { + Ok(removals.keys().cloned().collect()) +} + +fn resolve_installed_conflicts_for_subjects( + subjects: &[InstallConflictSubject], + rootfs: &Path, + config: &config::Config, + dry_run: bool, +) -> Result<()> { + for package in + prompt_installed_conflict_removals_for_subjects(subjects, rootfs, config, dry_run)? + { ui::info(format!("Removing conflicting package: {}", package)); - remove_installed_package_with_hooks(package, rootfs, config)?; + remove_installed_package_with_hooks(&package, rootfs, config)?; } Ok(()) @@ -1325,6 +1371,15 @@ fn install_planned_packages_to_rootfs( plans: &[PlannedPackageInstall], rootfs: &Path, config: &config::Config, +) -> Result<()> { + install_planned_packages_to_rootfs_with_pre_removed(plans, rootfs, config, &HashSet::new()) +} + +fn install_planned_packages_to_rootfs_with_pre_removed( + plans: &[PlannedPackageInstall], + rootfs: &Path, + config: &config::Config, + pre_removed_packages: &HashSet, ) -> Result<()> { let mut removed_replacements = HashSet::new(); let mut pending_post_hooks = Vec::new(); @@ -1338,6 +1393,9 @@ fn install_planned_packages_to_rootfs( plan.spec.package.revision )); for package in &plan.staged.replacement_removals { + if pre_removed_packages.contains(package) { + continue; + } if removed_replacements.insert(package.clone()) { remove_installed_package_with_hooks(package, rootfs, config)?; } @@ -2082,104 +2140,31 @@ impl Drop for SourceBuildCleanupGuard<'_> { } } -fn run_direct_install_request( +fn prepare_direct_install_request<'a>( options: DirectInstallOptions<'_>, - config: &config::Config, - mut spec_path: PathBuf, -) -> Result { - let mut install_lock = locking::open_lock(config)?; - let install_lock_path = locking::lock_path(config); - let _install_lock_guard = locking::try_write(&mut install_lock, &install_lock_path, "install")?; - - // Repo clone dir is available via `config.repo_clone_dir` and - // is passed explicitly to index builders below. - - // If the provided path doesn't exist, treat it as a package name and - // try to locate a spec under configured repo dir or local packages/. - if !spec_path.exists() { - let name = spec_path.to_string_lossy().to_string(); - ui::info(format!("Looking up package '{}' in local indexes...", name)); - let pkg_index = - index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone())); - if let Some(found) = pkg_index.find(&name) { - spec_path = found; - } else { - let host_arch = std::env::consts::ARCH; - let mut binary_repos: Vec<_> = config - .binary_repos - .iter() - .filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch)) - .collect(); - binary_repos.sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0))); - - for (repo_name, repo_cfg) in binary_repos { - match db::repo::find_binary_repo_package( - repo_name, - repo_cfg, - options.rootfs, - &config.package_cache_dir, - &name, - ) { - Ok(Some(rec)) => { - let archive = db::repo::fetch_binary_package_archive( - repo_name, - repo_cfg, - options.rootfs, - &rec, - &config.package_cache_dir, - )?; - ui::info(format!( - "Resolved '{}' from binary repo '{}' as {}-{} (package {}) ({} bytes){} -> {}", - name, - repo_name, - rec.version, - rec.revision, - rec.name, - rec.size, - rec.description - .as_ref() - .map(|d| format!(" [{}]", d)) - .unwrap_or_default(), - archive.display() - )); - spec_path = archive; - break; - } - Ok(None) => {} - Err(e) => { - crate::log_warn!("Binary repo '{}': {}", repo_name, e); - } - } - } - } - } - - let suppress_output = suppress_nested_install_output(); - if !suppress_output { - ui::info(format!("Installing package from: {}", spec_path.display())); - } - + config: &'a config::Config, + spec_path: &Path, + preparation: DirectInstallPreparationOptions<'_>, +) -> Result> { let (mut pkg_spec, staging_dir): (package::PackageSpec, Option) = if spec_path.to_string_lossy().ends_with(".tar.zst") { - // Install from archive - let (spec, tmp_dir) = load_package_archive_into_staging(config, &spec_path)?; + let (spec, tmp_dir) = load_package_archive_into_staging(config, spec_path)?; (spec, Some(tmp_dir)) } else { - // Install from spec (normal build) - let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?; + let mut pkg_spec = package::PackageSpec::from_file(spec_path)?; pkg_spec.apply_config(config); pkg_spec.build.flags.rootfs = build_cmd::build_env_rootfs(options.rootfs); (pkg_spec, None) }; let built_from_source = staging_dir.is_none(); - let _source_cleanup_guard = SourceBuildCleanupGuard::new(config, built_from_source); + let source_cleanup_guard = SourceBuildCleanupGuard::new(config, built_from_source); if options.lib32_only && staging_dir.is_some() { anyhow::bail!("--lib32-only is only supported when installing from a package spec"); } let lib32_only = effective_lib32_only(&pkg_spec, options.lib32_only); - if staging_dir.is_none() && !suppress_output { + if staging_dir.is_none() && !preparation.suppress_output { ui::info(format!( "Package: {} v{}-{}", pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision @@ -2204,30 +2189,37 @@ fn run_direct_install_request( if staging_dir.is_some() { conflict_subjects = install_conflict_subjects_for_spec(&pkg_spec, true, false); } - resolve_installed_conflicts_for_subjects( - &conflict_subjects, - options.rootfs, - config, - options.dry_run, - )?; + if preparation.resolve_installed_conflicts { + resolve_installed_conflicts_for_subjects( + &conflict_subjects, + options.rootfs, + config, + options.dry_run, + )?; + } if options.dry_run { ui::info("Dry run enabled, stopping before install/build work."); - return Ok(false); + return Ok(PreparedDirectInstall { + plans: Vec::new(), + resources: PreparedDirectInstallResources { + _staging_dir: staging_dir, + _source_cleanup_guard: source_cleanup_guard, + }, + }); } let install_targets = vec![format!( "{} v{}-{}", pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision )]; - if !suppress_output && !ui::prompt_package_action("installation", &install_targets, true)? { + if preparation.confirm_installation + && !preparation.suppress_output + && !ui::prompt_package_action("installation", &install_targets, true)? + { anyhow::bail!("Aborted"); } - let _snapper_pre_install_snapshot_todo: fn() -> ! = - || todo!("snapper: create pre-install snapshot before install work starts"); - - // Ensure database directory exists std::fs::create_dir_all(&config.db_dir).with_context(|| { format!( "Failed to create database directory: {}", @@ -2257,14 +2249,12 @@ fn run_direct_install_request( } } - // Check dependencies and prompt for auto-install if needed if !options.no_deps { let missing_required = merge_missing_dependencies( deps::check_build_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?, deps::check_runtime_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?, ); if !missing_required.is_empty() { - // Check for dependency cycles via DEPOT_DEPCHAIN env var let dep_chain = std::env::var("DEPOT_DEPCHAIN").unwrap_or_default(); let chain_set: std::collections::HashSet<&str> = dep_chain.split(',').filter(|s| !s.is_empty()).collect(); @@ -2302,11 +2292,9 @@ fn run_direct_install_request( dep_plan_packages }; if ui::prompt_package_action("dependency installation", &dep_prompt_packages, true)? { - // Build package index for fast lookups let pkg_index = index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone())); - // Build new dep chain let new_chain = if dep_chain.is_empty() { pkg_spec.package.name.clone() } else { @@ -2315,7 +2303,6 @@ fn run_direct_install_request( let mut dep_spec_paths = Vec::new(); for dep in missing_required { - // Use package index for O(1) lookup let candidate = pkg_index.find(&dep); if let Some(dep_spec_path) = candidate { @@ -2348,7 +2335,6 @@ fn run_direct_install_request( } } - // Enforce required dependencies before building/installing. deps::require_build_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?; deps::require_runtime_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?; if should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs) { @@ -2466,10 +2452,11 @@ fn run_direct_install_request( let destdir = if let Some(dir) = &staging_dir { dir.path().to_path_buf() } else { - // 1-2. Fetch + extract sources (supports archives and git URL#rev) - clean_build_source_dirs(config)?; + if preparation.clean_sources_before_build { + clean_build_source_dirs(config)?; + } source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?; - let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?; + let src_dir = source::prepare(&pkg_spec, &config.cache_dir, preparation.build_dir)?; built_src_dir = Some(src_dir.clone()); let host_build_dir = builder::ensure_host_build( &pkg_spec, @@ -2482,11 +2469,14 @@ fn run_direct_install_request( pkg_spec.build.flags.host_build_dir = Some(host_dir.to_string_lossy().into_owned()); } - // 3. Build - let destdir = config + let destdir = preparation .build_dir .join("destdir") .join(&pkg_spec.package.name); + if destdir.exists() { + fs::remove_dir_all(&destdir) + .with_context(|| format!("Failed to clean destdir: {}", destdir.display()))?; + } if !lib32_only { builder::build( @@ -2498,7 +2488,6 @@ fn run_direct_install_request( host_build_dir.as_deref(), )?; - // 3.1 Copy license files into staged tree staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?; install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?; builder::stage_generated_lifecycle_scripts(&pkg_spec, &destdir)?; @@ -2511,16 +2500,12 @@ fn run_direct_install_request( if !lib32_only { if staging_dir.is_none() { - // Source-build path: apply staging transforms (strip/compress/static cleanup). staging::process(&destdir, &pkg_spec)?; if let Some(src_dir) = built_src_dir.as_deref() { staging::stage_split_package_licenses(src_dir, &destdir, &pkg_spec)?; } - } else { - // Binary archive path: install as-packaged without post-build transformations. - if !suppress_output { - ui::info("Installing binary archive payload"); - } + } else if !preparation.suppress_output { + ui::info("Installing binary archive payload"); } let output_plans = @@ -2546,20 +2531,334 @@ fn run_direct_install_request( }); } - run_transaction_hooks_for_plans( - options.rootfs, - install::hooks::HookPhase::Pre, - &transaction_plans, + Ok(PreparedDirectInstall { + plans: transaction_plans, + resources: PreparedDirectInstallResources { + _staging_dir: staging_dir, + _source_cleanup_guard: source_cleanup_guard, + }, + }) +} + +fn install_direct_transaction( + plans: &[PlannedPackageInstall], + rootfs: &Path, + config: &config::Config, +) -> Result<()> { + run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Pre, plans)?; + install_planned_packages_to_rootfs(plans, rootfs, config)?; + run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Post, plans)?; + Ok(()) +} + +fn install_requests_for_plan( + plan: &planner::ExecutionPlan, + config: &config::Config, + rootfs: &Path, +) -> Result> { + let mut requests = Vec::new(); + for step in plan.actionable_steps() { + match &step.origin { + planner::PlanOrigin::Source { path, .. } => { + requests.push(path.clone()); + } + planner::PlanOrigin::Binary { repo_name, record } => { + let repo_cfg = config + .binary_repos + .get(repo_name) + .with_context(|| format!("Binary repo '{}' not found in config", repo_name))?; + let archive = db::repo::fetch_binary_package_archive( + repo_name, + repo_cfg, + rootfs, + record, + &config.package_cache_dir, + ) + .with_context(|| { + format!( + "Failed to fetch binary package '{}' from repo '{}'", + record.filename, repo_name + ) + })?; + requests.push(archive); + } + planner::PlanOrigin::Installed => {} + } + } + Ok(requests) +} + +fn planned_installed_removals( + rootfs: &Path, + config: &config::Config, + packages: impl IntoIterator, +) -> Result> { + let db_path = config.installed_db_path(rootfs); + let installed = db::get_installed_packages(&db_path)?; + let mut unique = BTreeSet::new(); + for package in packages { + if installed.contains(&package) { + unique.insert(package); + } + } + + unique + .into_iter() + .map(|package| { + let affected_paths = db::get_package_files(&db_path, &package)?; + Ok(PlannedInstalledRemoval { + package, + affected_paths, + }) + }) + .collect() +} + +fn transaction_contexts_for_update( + removals: &[PlannedInstalledRemoval], + plans: &[PlannedPackageInstall], +) -> Vec { + let mut contexts = Vec::with_capacity(removals.len() + plans.len()); + contexts.extend( + removals + .iter() + .map(|removal| install::hooks::HookExecutionContextOwned { + operation: install::hooks::HookOperation::Remove, + package: removal.package.clone(), + affected_paths: removal.affected_paths.clone(), + }), + ); + contexts.extend(plans.iter().map(|plan| plan.staged.hook_context.clone())); + contexts +} + +fn install_update_transaction( + plans: &[PlannedPackageInstall], + removals: &[PlannedInstalledRemoval], + rootfs: &Path, + config: &config::Config, +) -> Result<()> { + let contexts = transaction_contexts_for_update(removals, plans); + install::hooks::run_transaction_hooks_batch(rootfs, install::hooks::HookPhase::Pre, &contexts)?; + + for removal in removals { + ui::info(format!( + "Removing package for update transaction: {}", + removal.package + )); + remove_installed_package_without_transaction_hooks( + &removal.package, + rootfs, + config, + &removal.affected_paths, + )?; + } + + let pre_removed_packages: HashSet = removals + .iter() + .map(|removal| removal.package.clone()) + .collect(); + install_planned_packages_to_rootfs_with_pre_removed( + plans, + rootfs, + config, + &pre_removed_packages, )?; + install::hooks::run_transaction_hooks_batch( + rootfs, + install::hooks::HookPhase::Post, + &contexts, + )?; + Ok(()) +} + +fn run_direct_install_request( + options: DirectInstallOptions<'_>, + config: &config::Config, + mut spec_path: PathBuf, +) -> Result { + let mut install_lock = locking::open_lock(config)?; + let install_lock_path = locking::lock_path(config); + let _install_lock_guard = locking::try_write(&mut install_lock, &install_lock_path, "install")?; + + // Repo clone dir is available via `config.repo_clone_dir` and + // is passed explicitly to index builders below. + + // If the provided path doesn't exist, treat it as a package name and + // try to locate a spec under configured repo dir or local packages/. + if !spec_path.exists() { + let name = spec_path.to_string_lossy().to_string(); + ui::info(format!("Looking up package '{}' in local indexes...", name)); + let pkg_index = + index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone())); + if let Some(found) = pkg_index.find(&name) { + spec_path = found; + } else { + let host_arch = std::env::consts::ARCH; + let mut binary_repos: Vec<_> = config + .binary_repos + .iter() + .filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch)) + .collect(); + binary_repos.sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0))); + + for (repo_name, repo_cfg) in binary_repos { + match db::repo::find_binary_repo_package( + repo_name, + repo_cfg, + options.rootfs, + &config.package_cache_dir, + &name, + ) { + Ok(Some(rec)) => { + let archive = db::repo::fetch_binary_package_archive( + repo_name, + repo_cfg, + options.rootfs, + &rec, + &config.package_cache_dir, + )?; + ui::info(format!( + "Resolved '{}' from binary repo '{}' as {}-{} (package {}) ({} bytes){} -> {}", + name, + repo_name, + rec.version, + rec.revision, + rec.name, + rec.size, + rec.description + .as_ref() + .map(|d| format!(" [{}]", d)) + .unwrap_or_default(), + archive.display() + )); + spec_path = archive; + break; + } + Ok(None) => {} + Err(e) => { + crate::log_warn!("Binary repo '{}': {}", repo_name, e); + } + } + } + } + } + + let suppress_output = suppress_nested_install_output(); + if !suppress_output { + ui::info(format!("Installing package from: {}", spec_path.display())); + } + + let _snapper_pre_install_snapshot_todo: fn() -> ! = + || todo!("snapper: create pre-install snapshot before install work starts"); let _snapper_post_install_snapshot_todo: fn() -> ! = || todo!("snapper: create post-install snapshot after install commit succeeds"); - install_planned_packages_to_rootfs(&transaction_plans, options.rootfs, config)?; - run_transaction_hooks_for_plans( - options.rootfs, - install::hooks::HookPhase::Post, - &transaction_plans, - )?; + let prepared = prepare_direct_install_request( + options, + config, + &spec_path, + DirectInstallPreparationOptions { + build_dir: &config.build_dir, + clean_sources_before_build: true, + suppress_output, + confirm_installation: true, + resolve_installed_conflicts: true, + }, + )?; + if options.dry_run { + return Ok(false); + } + let _resources = prepared.resources; + install_direct_transaction(&prepared.plans, options.rootfs, config)?; + + Ok(true) +} + +fn isolated_update_build_dir(config: &config::Config, idx: usize) -> PathBuf { + config + .build_dir + .join("update-tx") + .join(format!("{:04}", idx + 1)) +} + +fn run_update_transaction_install_requests( + options: DirectInstallOptions<'_>, + config: &config::Config, + requests: &[PathBuf], +) -> Result { + if requests.is_empty() { + return Ok(false); + } + + let mut install_lock = locking::open_lock(config)?; + let install_lock_path = locking::lock_path(config); + let _install_lock_guard = locking::try_write(&mut install_lock, &install_lock_path, "update")?; + + let suppress_output = suppress_nested_install_output(); + if requests + .iter() + .any(|request| !is_archive_install_request(request)) + { + clean_build_source_dirs(config)?; + } + + let mut transaction_plans = Vec::new(); + let mut resources = Vec::with_capacity(requests.len()); + for (idx, request) in requests.iter().enumerate() { + if !suppress_output { + ui::info(format!( + "[{}/{}] preparing update payload from {}", + idx + 1, + requests.len(), + request.display() + )); + } + let build_dir = isolated_update_build_dir(config, idx); + let prepared = prepare_direct_install_request( + options, + config, + request, + DirectInstallPreparationOptions { + build_dir: &build_dir, + clean_sources_before_build: false, + suppress_output, + confirm_installation: false, + resolve_installed_conflicts: false, + }, + ) + .with_context(|| { + format!( + "Failed to prepare update payload from {}", + request.display() + ) + })?; + transaction_plans.extend(prepared.plans); + resources.push(prepared.resources); + } + + if options.dry_run { + return Ok(false); + } + + let conflict_subjects: Vec<_> = transaction_plans + .iter() + .flat_map(|plan| install_conflict_subjects_for_output_spec(&plan.spec)) + .collect(); + validate_no_transaction_conflicts(&conflict_subjects)?; + let mut removal_packages = prompt_installed_conflict_removals_for_subjects( + &conflict_subjects, + options.rootfs, + config, + false, + )?; + for plan in &transaction_plans { + removal_packages.extend(plan.staged.replacement_removals.iter().cloned()); + } + let removals = planned_installed_removals(options.rootfs, config, removal_packages)?; + + install_update_transaction(&transaction_plans, &removals, options.rootfs, config)?; + drop(resources); Ok(true) } diff --git a/src/commands/tests.rs b/src/commands/tests.rs index c210a70..565ff6e 100644 --- a/src/commands/tests.rs +++ b/src/commands/tests.rs @@ -41,6 +41,42 @@ fn lib32_args() -> Lib32Args { Lib32Args { lib32_only: false } } +fn write_basic_binary_archive( + archive_path: &Path, + package_name: &str, + version: &str, + revision: u32, + payload_path: &str, + payload: &[u8], +) -> Result<()> { + let file = fs::File::create(archive_path) + .with_context(|| format!("Failed to create {}", archive_path.display()))?; + let encoder = + zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?; + let mut tar = tar::Builder::new(encoder); + + let mut payload_header = tar::Header::new_gnu(); + payload_header.set_path(payload_path)?; + payload_header.set_size(payload.len() as u64); + payload_header.set_mode(0o755); + payload_header.set_cksum(); + tar.append(&payload_header, payload)?; + + let metadata = format!( + "name = \"{package_name}\"\nversion = \"{version}\"\nrevision = {revision}\ndescription = \"test\"\nhomepage = \"https://example.test\"\nlicense = \"MIT\"\n[dependencies]\nruntime = []\noptional = []\n" + ); + let mut meta_header = tar::Header::new_gnu(); + meta_header.set_path(".metadata.toml")?; + meta_header.set_size(metadata.len() as u64); + meta_header.set_mode(0o644); + meta_header.set_cksum(); + tar.append(&meta_header, metadata.as_bytes())?; + + let encoder = tar.into_inner()?; + encoder.finish()?; + Ok(()) +} + #[test] fn build_env_rootfs_uses_selected_non_live_rootfs() { let tmp = tempfile::tempdir().unwrap(); @@ -691,6 +727,95 @@ fn direct_archive_install_rejects_conflicting_archives_in_same_batch() -> Result Ok(()) } +#[test] +fn update_transaction_runs_matching_transaction_hook_once_for_batch() -> Result<()> { + let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?; + let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?; + let old_alpha = pkg_dir.path().join("alpha-1.0-1-x86_64.depot.pkg.tar.zst"); + let old_beta = pkg_dir.path().join("beta-1.0-1-x86_64.depot.pkg.tar.zst"); + let new_alpha = pkg_dir.path().join("alpha-2.0-1-x86_64.depot.pkg.tar.zst"); + let new_beta = pkg_dir.path().join("beta-2.0-1-x86_64.depot.pkg.tar.zst"); + write_basic_binary_archive(&old_alpha, "alpha", "1.0", 1, "usr/bin/alpha", b"alpha-old")?; + write_basic_binary_archive(&old_beta, "beta", "1.0", 1, "usr/bin/beta", b"beta-old")?; + write_basic_binary_archive(&new_alpha, "alpha", "2.0", 1, "usr/bin/alpha", b"alpha-new")?; + write_basic_binary_archive(&new_beta, "beta", "2.0", 1, "usr/bin/beta", b"beta-new")?; + + let mut cfg = config::Config::for_rootfs(rootfs.path()); + cfg.build_dir = rootfs.path().join("var/cache/depot/build"); + cfg.db_dir = rootfs.path().join("var/lib/depot"); + + run_direct_archive_install_requests( + DirectInstallOptions { + rootfs: rootfs.path(), + no_deps: true, + no_flags: false, + cross_prefix: None, + clean: false, + dry_run: false, + lib32_only: false, + install_test_deps: false, + }, + &cfg, + &[old_alpha, old_beta], + false, + )?; + + let hooks_dir = install::hooks::transaction_hooks_dir(rootfs.path()); + fs::create_dir_all(&hooks_dir)?; + fs::write( + hooks_dir.join("90-update-batch.toml"), + r#" +[hook] +name = "update batch recorder" + +[when] +phase = "post" +operation = ["update"] +paths = ["usr/bin/*"] + +[exec] +command = "printf '%s:%s\n' \"$DEPOT_ACTION\" \"$DEPOT_PACKAGE\" >> \"$DEPOT_ROOTFS/hook-runs\"; cat >> \"$DEPOT_ROOTFS/hook-targets\"" +needs_paths = true +"#, + )?; + + let updated = run_update_transaction_install_requests( + DirectInstallOptions { + rootfs: rootfs.path(), + no_deps: true, + no_flags: false, + cross_prefix: None, + clean: false, + dry_run: false, + lib32_only: false, + install_test_deps: false, + }, + &cfg, + &[new_alpha, new_beta], + )?; + + assert!(updated); + assert_eq!( + fs::read_to_string(rootfs.path().join("usr/bin/alpha"))?, + "alpha-new" + ); + assert_eq!( + fs::read_to_string(rootfs.path().join("usr/bin/beta"))?, + "beta-new" + ); + let hook_runs = fs::read_to_string(rootfs.path().join("hook-runs"))?; + assert_eq!(hook_runs.lines().collect::>(), vec!["update:alpha"]); + let hook_targets: BTreeSet<_> = fs::read_to_string(rootfs.path().join("hook-targets"))? + .lines() + .map(str::to_string) + .collect(); + assert_eq!( + hook_targets, + BTreeSet::from(["usr/bin/alpha".to_string(), "usr/bin/beta".to_string()]) + ); + Ok(()) +} + #[test] fn collect_conflicting_installed_packages_matches_by_name_and_provide() -> Result<()> { let removals = collect_conflicting_installed_packages( diff --git a/src/commands/update/candidates.rs b/src/commands/update/candidates.rs index c80e049..d85e47f 100644 --- a/src/commands/update/candidates.rs +++ b/src/commands/update/candidates.rs @@ -747,29 +747,6 @@ pub(crate) fn collect_missing_update_dependencies( Ok(missing) } -fn run_update_install_command( - program: &Path, - request: &Path, - options: UpdateCommandOptions<'_>, -) -> Result<()> { - super::super::run_install_command_with_program( - program, - &[request.to_path_buf()], - options.rootfs, - super::super::ChildInstallCommandOptions { - no_deps: true, - assume_yes: true, - no_flags: options.no_flags, - cross_prefix: options.cross_prefix, - clean: options.clean, - lib32_only: false, - install_test_deps: options.install_test_deps, - install_context: Some(super::super::INSTALL_CONTEXT_UPDATE), - dep_chain: None, - }, - ) -} - fn confirm_update_replacements(candidates: &[UpdateCandidate]) -> Result<()> { for candidate in candidates { if !candidate.replaces_installed { @@ -865,13 +842,16 @@ pub(crate) fn run_update_command( confirm_update_replacements(&updates)?; } - super::super::resolve_installed_conflicts_for_subjects( - &conflict_subjects, - options.rootfs, - config, - options.dry_run, - )?; + if options.dry_run { + super::super::resolve_installed_conflicts_for_subjects( + &conflict_subjects, + options.rootfs, + config, + true, + )?; + } + let mut transaction_requests = Vec::new(); if !missing_deps.is_empty() { let dep_plan = planner::build_dependency_install_plan( config, @@ -892,29 +872,19 @@ pub(crate) fn run_update_command( return Ok(()); } - crate::commands::execute_install_plan_with_child_commands( + transaction_requests.extend(super::super::install_requests_for_plan( &dep_plan, - options.rootfs, config, - crate::commands::InstallPlanExecutionOptions { - no_flags: options.no_flags, - cross_prefix: options.cross_prefix, - clean: options.clean, - dry_run: false, - confirm_installation: false, - lib32_only_requested_specs: false, - install_test_deps: options.install_test_deps, - }, - )?; + options.rootfs, + )?); } else if options.dry_run { ui::info("Dry run enabled, stopping before update."); return Ok(()); } - let exe = std::env::current_exe().context("Failed to locate depot executable")?; for (idx, candidate) in updates.iter().enumerate() { ui::info(format!( - "[{}/{}] updating {} v{}-{} -> {} v{}-{}", + "[{}/{}] staging update {} v{}-{} -> {} v{}-{}", idx + 1, updates.len(), candidate.installed_package, @@ -925,8 +895,22 @@ pub(crate) fn run_update_command( candidate.candidate_revision )); let request = candidate_request_path(candidate, config, options.rootfs)?; - run_update_install_command(&exe, &request, options)?; + transaction_requests.push(request); } + super::super::run_update_transaction_install_requests( + super::super::DirectInstallOptions { + rootfs: options.rootfs, + no_deps: true, + no_flags: options.no_flags, + cross_prefix: options.cross_prefix, + clean: options.clean, + dry_run: false, + lib32_only: false, + install_test_deps: options.install_test_deps, + }, + config, + &transaction_requests, + )?; if options.clean { crate::commands::clean_build_workspace(config)?;