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)?;