diff --git a/.php-version b/.php-version new file mode 100644 index 0000000..0b95416 --- /dev/null +++ b/.php-version @@ -0,0 +1 @@ +8.4.18 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f70f92c..d99ac19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,9 +112,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-sys", "zeroize", @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "cc", "cmake", @@ -319,50 +319,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "cssparser" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - [[package]] name = "dialoguer" version = "0.12.0" @@ -413,33 +369,12 @@ dependencies = [ "syn", ] -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" -[[package]] -name = "ego-tree" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" - [[package]] name = "encode_unicode" version = "1.0.0" @@ -558,20 +493,20 @@ dependencies = [ ] [[package]] -name = "fs_extra" -version = "1.3.0" +name = "fs4" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] [[package]] -name = "futf" -version = "0.1.5" +name = "fs_extra" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" @@ -633,15 +568,6 @@ dependencies = [ "slab", ] -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -664,20 +590,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -722,16 +648,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "html5ever" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" -dependencies = [ - "log", - "markup5ever", -] - [[package]] name = "http" version = "1.4.0" @@ -976,9 +892,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -1004,9 +920,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -1017,9 +933,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -1060,9 +976,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1082,20 +998,21 @@ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1124,23 +1041,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "markup5ever" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - [[package]] name = "memchr" version = "2.8.0" @@ -1199,12 +1099,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1273,59 +1167,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - [[package]] name = "php-version-manager" version = "1.0.4" @@ -1338,26 +1179,25 @@ dependencies = [ "dirs", "env_logger", "flate2", + "fs4", + "futures-util", "indicatif", - "log", "mockito", "predicates", - "regex", "reqwest", - "scraper", + "semver", "serde", "serde_json", "tar", "tempfile", - "thiserror 2.0.18", "tokio", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1365,6 +1205,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1398,12 +1244,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "predicates" version = "3.1.4" @@ -1511,9 +1351,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1524,6 +1364,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.2" @@ -1564,9 +1410,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags", ] @@ -1607,9 +1453,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -1638,6 +1484,8 @@ dependencies = [ "rustls", "rustls-pki-types", "rustls-platform-verifier", + "serde", + "serde_json", "sync_wrapper", "tokio", "tokio-rustls", @@ -1672,20 +1520,11 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -1696,9 +1535,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "once_cell", @@ -1805,21 +1644,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scraper" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93cecd86d6259499c844440546d02f55f3e17bd286e529e48d1f9f67e92315cb" -dependencies = [ - "cssparser", - "ego-tree", - "getopts", - "html5ever", - "precomputed-hash", - "selectors", - "tendril", -] - [[package]] name = "security-framework" version = "3.7.0" @@ -1843,25 +1667,6 @@ dependencies = [ "libc", ] -[[package]] -name = "selectors" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" -dependencies = [ - "bitflags", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - [[package]] name = "semver" version = "1.0.27" @@ -1923,15 +1728,6 @@ dependencies = [ "serde", ] -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "shell-words" version = "1.1.1" @@ -1966,12 +1762,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - [[package]] name = "slab" version = "0.4.12" @@ -1986,12 +1776,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2000,30 +1790,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - [[package]] name = "strsim" version = "0.11.1" @@ -2101,28 +1867,17 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", ] -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - [[package]] name = "termtree" version = "0.5.1" @@ -2196,9 +1951,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -2213,9 +1968,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -2357,12 +2112,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -2429,9 +2178,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2442,9 +2191,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.61" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -2456,9 +2205,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2466,9 +2215,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -2479,9 +2228,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -2535,9 +2284,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -2553,18 +2302,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web_atoms" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - [[package]] name = "webpki-root-certs" version = "1.0.6" @@ -2636,6 +2373,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -2969,18 +2715,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b2704a2..87f3ff0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,22 +18,21 @@ dialoguer = "0.12.0" dirs = "6.0.0" env_logger = "0.11.9" flate2 = "1.1.9" +fs4 = "0.13.1" +futures-util = "0.3.32" indicatif = "0.18.4" -log = "0.4.29" -regex = "1.12.3" -reqwest = { version = "0.13.2", features = ["stream", "rustls"] } -scraper = "0.25.0" +reqwest = { version = "0.13.2", features = ["stream", "rustls", "json"] } +semver = "1.0.27" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" tar = "0.4.44" -thiserror = "2.0.18" -tokio = { version = "1.49.0", features = ["full"] } +tokio = { version = "1.50.0", features = ["full"] } [dev-dependencies] assert_cmd = "2.1.2" mockito = "1.7.2" predicates = "3.1.4" -tempfile = "3.25.0" +tempfile = "3.26.0" # Optimize for binary size and performance [profile.release] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..56855ea --- /dev/null +++ b/build.rs @@ -0,0 +1,41 @@ +use std::process::Command; + +fn main() { + let commit_hash = Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output() + .ok() + .and_then(|output| String::from_utf8(output.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let build_time = Command::new("date") + .args(["-u", "+%Y-%m-%dT%H:%M:%SZ"]) + .output() + .ok() + .and_then(|output| String::from_utf8(output.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let is_ci = std::env::var("GITHUB_ACTIONS").is_ok(); + + let version = if is_ci { + let tag = Command::new("git") + .args(["describe", "--tags", "--always"]) + .output() + .ok() + .and_then(|output| String::from_utf8(output.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + format!("{} (built at: {})", tag, build_time) + } else { + let pkg_version = + std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string()); + format!( + "{} (commit: {}, built at: {})", + pkg_version, commit_hash, build_time + ) + }; + + println!("cargo:rustc-env=PVM_VERSION={}", version); +} diff --git a/src/cli.rs b/src/cli.rs index 0f6c062..7c06cf0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,7 +5,7 @@ use clap::{Parser, Subcommand}; #[derive(Parser)] #[command( name = "pvm", - version = env!("CARGO_PKG_VERSION"), + version = env!("PVM_VERSION"), author, about = "Fast and simple PHP version manager", disable_version_flag = true, diff --git a/src/commands/env.rs b/src/commands/env.rs index 7671b12..913d72d 100644 --- a/src/commands/env.rs +++ b/src/commands/env.rs @@ -1,3 +1,4 @@ +use crate::constants::PVM_DIR_VAR; use crate::shell; use anyhow::Result; use clap::Parser; @@ -18,6 +19,7 @@ pub struct Env { impl Env { pub async fn call(self) -> Result<()> { + let pvm_dir = crate::fs::get_pvm_dir()?; let s: Box = match self.shell.as_deref() { Some("bash") => Box::new(shell::Bash), Some("zsh") => Box::new(shell::Zsh), @@ -25,6 +27,7 @@ impl Env { _ => shell::detect_shell(), }; + println!("{}", s.set_env_var(PVM_DIR_VAR, &pvm_dir.to_string_lossy())); println!("{}", s.wrapper_fn()); println!("{}", s.use_on_cd()); diff --git a/src/commands/install.rs b/src/commands/install.rs index ea3d7b0..1ed8219 100644 --- a/src/commands/install.rs +++ b/src/commands/install.rs @@ -1,7 +1,10 @@ +use crate::constants::MULTISHELL_PATH_VAR; use crate::{fs, network}; use anyhow::Result; use clap::Parser; use colored::Colorize; +use fs4::fs_std::FileExt; +use std::io::Write; /// Install a specific PHP version #[derive(Parser, Debug)] @@ -72,23 +75,33 @@ pub async fn execute_install(version: &str) -> Result<()> { if let Ok(bin_dir) = crate::fs::get_version_bin_dir(&v) { let s = crate::shell::detect_shell(); let export_str1 = - s.set_env_var("PVM_MULTISHELL_PATH", &bin_dir.to_string_lossy()); + s.set_env_var(MULTISHELL_PATH_VAR, &bin_dir.to_string_lossy()); let export_str2 = s.path(&bin_dir); - if let Ok(pvm_dir) = crate::fs::get_pvm_dir() { - let env_file = pvm_dir.join(".env_update"); - std::fs::write( - &env_file, - format!("{}\n{}", export_str1, export_str2), - ) - .ok(); + if let Ok(env_file) = crate::fs::get_env_update_path() { + // Atomic write with advisory lock + if let Ok(file) = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&env_file) + { + file.lock_exclusive().ok(); + let mut writer = std::io::BufWriter::new(&file); + writeln!(writer, "{}\n{}", export_str1, export_str2).ok(); + writer.flush().ok(); + file.unlock().ok(); + } } unsafe { - std::env::set_var("PVM_MULTISHELL_PATH", &bin_dir); + std::env::set_var(MULTISHELL_PATH_VAR, &bin_dir); if let Some(path) = std::env::var_os("PATH") { let mut new_path = std::ffi::OsString::new(); new_path.push(&bin_dir); + #[cfg(windows)] + new_path.push(";"); + #[cfg(not(windows))] new_path.push(":"); new_path.push(&path); std::env::set_var("PATH", new_path); diff --git a/src/commands/use_cmd.rs b/src/commands/use_cmd.rs index 4f0be51..799f6eb 100644 --- a/src/commands/use_cmd.rs +++ b/src/commands/use_cmd.rs @@ -1,8 +1,11 @@ +use crate::constants::{MULTISHELL_PATH_VAR, PHP_VERSION_FILE}; use crate::{fs, shell, update}; use anyhow::Result; use clap::Parser; use colored::Colorize; use dialoguer::{Confirm, Select, theme::ColorfulTheme}; +use fs4::fs_std::FileExt; +use std::io::Write; use std::path::Path; /// Change PHP version @@ -71,12 +74,13 @@ impl Use { } // Smart prompt logic - if Path::new(".php-version").exists() - && let Ok(current_file_ver) = std::fs::read_to_string(".php-version") + if Path::new(PHP_VERSION_FILE).exists() + && let Ok(current_file_ver) = std::fs::read_to_string(PHP_VERSION_FILE) && current_file_ver.trim() != version { let prompt = format!( - "A .php-version file is present ({}). Do you want to apply this change to the directory?", + "A {} file is present ({}). Do you want to apply this change to the directory?", + PHP_VERSION_FILE, current_file_ver.trim().yellow() ); if Confirm::with_theme(&ColorfulTheme::default()) @@ -85,8 +89,13 @@ impl Use { .interact_opt()? .unwrap_or(false) { - std::fs::write(".php-version", &version).ok(); - eprintln!("{} Updated .php-version to {}", "✓".green(), version.bold()); + std::fs::write(PHP_VERSION_FILE, &version).ok(); + eprintln!( + "{} Updated {} to {}", + "✓".green(), + PHP_VERSION_FILE, + version.bold() + ); } } @@ -94,19 +103,33 @@ impl Use { let s = shell::detect_shell(); // These evaluate in the user's shell hook via wrapper - let export_str1 = s.set_env_var("PVM_MULTISHELL_PATH", &bin_dir.to_string_lossy()); + let export_str1 = s.set_env_var(MULTISHELL_PATH_VAR, &bin_dir.to_string_lossy()); let export_str2 = s.path(&bin_dir); - let pvm_dir = fs::get_pvm_dir()?; - let env_file = pvm_dir.join(".env_update"); - std::fs::write(&env_file, format!("{}\n{}", export_str1, export_str2)).ok(); + let env_file = fs::get_env_update_path()?; + + // Atomic write with advisory lock + let file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&env_file)?; + file.lock_exclusive()?; + let mut writer = std::io::BufWriter::new(&file); + writeln!(writer, "{}", export_str1)?; + writeln!(writer, "{}", export_str2)?; + writer.flush()?; + file.unlock()?; // Also update the current Rust binary's environment so spawned subs (or interactive loop) see it unsafe { - std::env::set_var("PVM_MULTISHELL_PATH", &bin_dir); + std::env::set_var(MULTISHELL_PATH_VAR, &bin_dir); if let Some(path) = std::env::var_os("PATH") { let mut new_path = std::ffi::OsString::new(); new_path.push(&bin_dir); + #[cfg(windows)] + new_path.push(";"); + #[cfg(not(windows))] new_path.push(":"); new_path.push(&path); std::env::set_var("PATH", new_path); diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..7d58c50 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,20 @@ +/// The name of the environment variable used to store the path to the PVM installation. +pub const PVM_DIR_VAR: &str = "PVM_DIR"; + +/// The name of the environment variable used to store the path to the currently active PHP version's bin directory. +pub const MULTISHELL_PATH_VAR: &str = "PVM_MULTISHELL_PATH"; + +/// The name of the file used to store the environment variables to be updated in the shell. +pub const ENV_UPDATE_FILE: &str = ".env_update"; + +/// The name of the file used to store the remote versions cache. +pub const REMOTE_CACHE_FILE: &str = "remote_cache.json"; + +/// The name of the file used as a guard for the update check. +pub const UPDATE_CHECK_GUARD_FILE: &str = ".update_check_guard"; + +/// The name of the file used to store the PHP version for a directory. +pub const PHP_VERSION_FILE: &str = ".php-version"; + +/// The base URL for fetching available PHP versions. +pub const BASE_URL: &str = "https://dl.static-php.dev/static-php-cli/common/"; diff --git a/src/fs.rs b/src/fs.rs index 7e88824..52386be 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,3 +1,4 @@ +use crate::constants::{MULTISHELL_PATH_VAR, PVM_DIR_VAR}; use anyhow::{Context, Result}; use std::path::PathBuf; @@ -8,7 +9,7 @@ pub struct VersionItem { } pub fn get_pvm_dir() -> Result { - if let Ok(pvm_dir) = std::env::var("PVM_DIR") { + if let Ok(pvm_dir) = std::env::var(PVM_DIR_VAR) { return Ok(PathBuf::from(pvm_dir)); } let home = dirs::data_local_dir().context("Could not find local data directory")?; @@ -44,16 +45,12 @@ pub fn list_installed_versions() -> Result> { } } - versions.sort_by(|a, b| { - let a_parts: Vec = a.split('.').filter_map(|s| s.parse().ok()).collect(); - let b_parts: Vec = b.split('.').filter_map(|s| s.parse().ok()).collect(); - a_parts.cmp(&b_parts) - }); + crate::utils::sort_versions(&mut versions); Ok(versions) } pub fn get_current_version() -> String { - if let Ok(path) = std::env::var("PVM_MULTISHELL_PATH") { + if let Ok(path) = std::env::var(MULTISHELL_PATH_VAR) { let p = PathBuf::from(path); if let Some(parent) = p.parent() && let Some(name) = parent.file_name() @@ -64,6 +61,17 @@ pub fn get_current_version() -> String { "system".to_string() } +pub fn get_env_update_path() -> Result { + let pvm_dir = get_pvm_dir()?; + let shell_pid = std::env::var("PVM_SHELL_PID").unwrap_or_default(); + let filename = if shell_pid.is_empty() { + crate::constants::ENV_UPDATE_FILE.to_string() + } else { + format!("{}_{}", crate::constants::ENV_UPDATE_FILE, shell_pid) + }; + Ok(pvm_dir.join(filename)) +} + pub fn get_aliased_versions() -> Result> { let mut installed = list_installed_versions()?; if installed.is_empty() { diff --git a/src/main.rs b/src/main.rs index 664005a..6724f55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ mod cli; mod commands; +mod constants; mod fs; mod interactive; mod network; mod shell; mod update; +mod utils; use anyhow::Result; use clap::Parser; diff --git a/src/network.rs b/src/network.rs index fb15a3b..4828d89 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,7 +1,10 @@ +use crate::constants::{BASE_URL, REMOTE_CACHE_FILE}; use anyhow::{Context, Result}; use flate2::read::GzDecoder; +use fs4::fs_std::FileExt; +use futures_util::StreamExt; use reqwest::Client; -use scraper::{Html, Selector}; +use serde::Deserialize; use std::path::Path; use tar::Archive; @@ -11,24 +14,44 @@ use std::fs::File; use std::io::{Read, Write}; use std::time::Duration; -const BASE_URL: &str = "https://dl.static-php.dev/static-php-cli/common/"; -const CACHE_FILE: &str = "remote_cache.json"; const CACHE_DURATION: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours +#[derive(Deserialize)] +struct RemoteFile { + name: String, + is_dir: bool, +} + +fn get_target_triple() -> Result<&'static str> { + use std::env::consts::{ARCH, OS}; + match (OS, ARCH) { + ("linux", "x86_64") => Ok("linux-x86_64"), + ("linux", "aarch64") => Ok("linux-aarch64"), + ("macos", "x86_64") => Ok("macos-x86_64"), + ("macos", "aarch64") => Ok("macos-aarch64"), + _ => anyhow::bail!("Unsupported OS/Architecture: {}-{}", OS, ARCH), + } +} + pub async fn get_available_versions() -> Result> { let pvm_dir = crate::fs::get_pvm_dir()?; - let cache_path = pvm_dir.join(CACHE_FILE); + let cache_path = pvm_dir.join(REMOTE_CACHE_FILE); // 1. Try to load from valid cache if cache_path.exists() - && let Ok(metadata) = std::fs::metadata(&cache_path) - && let Ok(modified) = metadata.modified() - && let Ok(elapsed) = modified.elapsed() - && elapsed < CACHE_DURATION - && let Ok(mut file) = File::open(&cache_path) + && let Ok(file) = File::open(&cache_path) { + file.lock_shared().ok(); let mut contents = String::new(); - if file.read_to_string(&mut contents).is_ok() + let mut f = &file; + let read_res = f.read_to_string(&mut contents); + file.unlock().ok(); + + if read_res.is_ok() + && let Ok(metadata) = std::fs::metadata(&cache_path) + && let Ok(modified) = metadata.modified() + && let Ok(elapsed) = modified.elapsed() + && elapsed < CACHE_DURATION && let Ok(versions) = serde_json::from_str::>(&contents) { return Ok(versions); @@ -50,45 +73,47 @@ pub async fn get_available_versions() -> Result> { spinner.enable_steady_tick(Duration::from_millis(100)); let client = Client::new(); - let res = client.get(BASE_URL).send().await?.text().await?; + let json_url = format!("{}?format=json", BASE_URL); + let res = client + .get(json_url) + .send() + .await + .context("Failed to fetch version list from remote")? + .error_for_status() + .context("Remote server returned an error when fetching version list")? + .json::>() + .await + .context("Failed to parse remote version JSON")?; spinner.finish_and_clear(); - let document = Html::parse_document(&res); - let selector = Selector::parse("a").unwrap(); + let target = get_target_triple()?; + let suffix = format!("-cli-{}.tar.gz", target); let mut versions = Vec::new(); - for element in document.select(&selector) { - let href = element.value().attr("href").unwrap_or(""); - if href.contains("/php-") && href.ends_with("-cli-linux-x86_64.tar.gz") { - let filename = href.rsplit('/').next().unwrap_or(href); - if let Some(version) = filename + for file in res { + if !file.is_dir + && file.name.starts_with("php-") + && file.name.ends_with(&suffix) + && let Some(version) = file + .name .strip_prefix("php-") - .and_then(|h| h.strip_suffix("-cli-linux-x86_64.tar.gz")) - { - versions.push(version.to_string()); - } - } else if href.starts_with("php-") - && href.ends_with("-cli-linux-x86_64.tar.gz") - && let Some(version) = href - .strip_prefix("php-") - .and_then(|h| h.strip_suffix("-cli-linux-x86_64.tar.gz")) + .and_then(|h: &str| h.strip_suffix(&suffix)) { versions.push(version.to_string()); } } - // Sort versions by splitting by dot and parsing as numbers - versions.sort_by(|a, b| { - let a_parts: Vec = a.split('.').filter_map(|s| s.parse().ok()).collect(); - let b_parts: Vec = b.split('.').filter_map(|s| s.parse().ok()).collect(); - a_parts.cmp(&b_parts) - }); + crate::utils::sort_versions(&mut versions); // 3. Write to cache if let Ok(json) = serde_json::to_string(&versions) { std::fs::create_dir_all(&pvm_dir).ok(); - if let Ok(mut file) = File::create(&cache_path) { - file.write_all(json.as_bytes()).ok(); + if let Ok(file) = File::create(&cache_path) { + file.lock_exclusive().ok(); + let mut writer = std::io::BufWriter::new(&file); + writer.write_all(json.as_bytes()).ok(); + writer.flush().ok(); + file.unlock().ok(); } } @@ -127,22 +152,49 @@ pub async fn resolve_version(requested: &str) -> Result { } pub async fn download_and_extract(resolved_version: &str, dest: &Path) -> Result<()> { - let url = format!( - "{}/php-{}-cli-linux-x86_64.tar.gz", - BASE_URL, resolved_version - ); + let target = get_target_triple()?; + let url = format!("{}php-{}-cli-{}.tar.gz", BASE_URL, resolved_version, target); let client = Client::new(); - let res = client.get(&url).send().await?.bytes().await?; + let response = client + .get(&url) + .send() + .await + .context("Failed to connect to download server")? + .error_for_status() + .context("Server returned an error for the requested PHP version")?; + + let total_size = response + .content_length() + .context("Failed to get content length from server")?; + + let pb = ProgressBar::new(total_size); + pb.set_style(ProgressStyle::default_bar() + .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")? + .progress_chars("#>-")); + + let mut downloaded: u64 = 0; + let mut stream = response.bytes_stream(); + let mut buffer = Vec::new(); + + while let Some(item) = stream.next().await { + let chunk = item.context("Error while downloading chunk")?; + buffer.extend_from_slice(&chunk); + let new = std::cmp::min(downloaded + (chunk.len() as u64), total_size); + downloaded = new; + pb.set_position(new); + } + + pb.finish_with_message("Download complete"); - let cursor = std::io::Cursor::new(res); + let cursor = std::io::Cursor::new(buffer); let tar = GzDecoder::new(cursor); let mut archive = Archive::new(tar); let bin_dir = dest.join("bin"); std::fs::create_dir_all(&bin_dir)?; - archive - .unpack(&bin_dir) - .context("Failed to unpack downloaded archive")?; + archive.unpack(&bin_dir).context( + "Failed to unpack downloaded archive - the file might be corrupted or incomplete", + )?; // Make it executable #[cfg(unix)] diff --git a/src/shell.rs b/src/shell.rs index e832a9c..6c6557f 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,3 +1,4 @@ +use crate::constants::{ENV_UPDATE_FILE, PVM_DIR_VAR}; use std::path::Path; pub trait Shell { @@ -35,27 +36,33 @@ fi } fn wrapper_fn(&self) -> String { - " -export PATH=\"$HOME/.local/share/pvm/bin:$PATH\" + format!( + " +export PATH=\"${{{}}}/bin:$PATH\" -pvm() { +pvm() {{ local command=$1 if [[ \"$command\" == \"env\" ]]; then command pvm \"$@\" else - local env_file=\"$PVM_DIR/.env_update\" - rm -f \"$env_file\" 2>/dev/null - command pvm \"$@\" - local exit_code=$? - if [[ -f \"$env_file\" ]]; then - eval \"$(cat \"$env_file\")\" - rm -f \"$env_file\" + if [[ -n \"${{{}}}\" && -d \"${{{}}}\" ]]; then + local env_file=\"${{{}}}/{}_$$\" + [[ -f \"$env_file\" ]] && command rm -f \"$env_file\" 2>/dev/null + PVM_SHELL_PID=$$ command pvm \"$@\" + local exit_code=$? + if [[ -f \"$env_file\" ]]; then + eval \"$(cat \"$env_file\")\" + command rm -f \"$env_file\" 2>/dev/null + fi + return $exit_code + else + command pvm \"$@\" fi - return $exit_code fi -} -" - .to_string() +}} +", + PVM_DIR_VAR, PVM_DIR_VAR, PVM_DIR_VAR, PVM_DIR_VAR, ENV_UPDATE_FILE + ) } } @@ -84,27 +91,33 @@ add-zsh-hook chpwd _pvm_cd_hook } fn wrapper_fn(&self) -> String { - " -export PATH=\"$HOME/.local/share/pvm/bin:$PATH\" + format!( + " +export PATH=\"${{{}}}/bin:$PATH\" -pvm() { +pvm() {{ local command=$1 if [[ \"$command\" == \"env\" ]]; then command pvm \"$@\" else - local env_file=\"$PVM_DIR/.env_update\" - rm -f \"$env_file\" 2>/dev/null - command pvm \"$@\" - local exit_code=$? - if [[ -f \"$env_file\" ]]; then - eval \"$(cat \"$env_file\")\" - rm -f \"$env_file\" + if [[ -n \"${{{}}}\" && -d \"${{{}}}\" ]]; then + local env_file=\"${{{}}}/{}_$$\" + [[ -f \"$env_file\" ]] && command rm -f \"$env_file\" 2>/dev/null + PVM_SHELL_PID=$$ command pvm \"$@\" + local exit_code=$? + if [[ -f \"$env_file\" ]]; then + eval \"$(cat \"$env_file\")\" + command rm -f \"$env_file\" 2>/dev/null + fi + return $exit_code + else + command pvm \"$@\" fi - return $exit_code fi -} -" - .to_string() +}} +", + PVM_DIR_VAR, PVM_DIR_VAR, PVM_DIR_VAR, PVM_DIR_VAR, ENV_UPDATE_FILE + ) } } @@ -131,27 +144,35 @@ end } fn wrapper_fn(&self) -> String { - " -set -gx PATH \"$HOME/.local/share/pvm/bin\" $PATH + format!( + " +set -gx PATH \"${{{}}}/bin\" $PATH function pvm set command $argv[1] if test \"$command\" = \"env\" command pvm $argv else - set env_file \"$PVM_DIR/.env_update\" - rm -f \"$env_file\" 2>/dev/null - command pvm $argv - set exit_code $status - if test -f \"$env_file\" - eval (cat \"$env_file\") - rm -f \"$env_file\" + if test -n \"${{{}}}\"; and test -d \"${{{}}}\" + set env_file \"${{{}}}/{}_$fish_pid\" + if test -f \"$env_file\" + command rm -f \"$env_file\" &>/dev/null + end + PVM_SHELL_PID=$fish_pid command pvm $argv + set exit_code $status + if test -f \"$env_file\" + eval (cat \"$env_file\") + command rm -f \"$env_file\" &>/dev/null + end + return $exit_code + else + command pvm $argv end - return $exit_code end end -" - .to_string() +", + PVM_DIR_VAR, PVM_DIR_VAR, PVM_DIR_VAR, PVM_DIR_VAR, ENV_UPDATE_FILE + ) } } diff --git a/src/update.rs b/src/update.rs index 4810276..ea521a4 100644 --- a/src/update.rs +++ b/src/update.rs @@ -1,5 +1,8 @@ +use crate::constants::UPDATE_CHECK_GUARD_FILE; use crate::{fs, network}; use anyhow::Result; +use fs4::fs_std::FileExt; +use std::io::{Read, Write}; use std::time::{SystemTime, UNIX_EPOCH}; pub async fn check_for_updates(target_version: &str) -> Result> { @@ -9,20 +12,37 @@ pub async fn check_for_updates(target_version: &str) -> Result> { } let pvm_dir = fs::get_pvm_dir()?; - let guard_file = pvm_dir.join(".update_check_guard"); + let guard_file = pvm_dir.join(UPDATE_CHECK_GUARD_FILE); + + // Acquire lock and check if 24 hours have passed + let file = std::fs::OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(true) + .open(&guard_file)?; + + file.lock_exclusive()?; + + let mut contents = String::new(); + let mut f = &file; + f.read_to_string(&mut contents).ok(); - // Check if 24 hours have passed let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - if guard_file.exists() - && let Ok(contents) = std::fs::read_to_string(&guard_file) + if !contents.is_empty() && let Ok(last_check) = contents.trim().parse::() && now - last_check < 86400 { + file.unlock().ok(); return Ok(None); } // Write the new timestamp to prevent spam on next commands - std::fs::write(&guard_file, now.to_string()).ok(); + file.set_len(0).ok(); + let mut writer = std::io::BufWriter::new(&file); + writeln!(writer, "{}", now).ok(); + writer.flush().ok(); + file.unlock().ok(); if target_version == "system" { return Ok(None); diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..7e24eab --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,20 @@ +use semver::Version; + +/// Sorts a list of version strings using semantic versioning. +/// If a version string is not valid semver, it falls back to a simple string-based numeric sort. +pub fn sort_versions(versions: &mut [String]) { + versions.sort_by(|a, b| { + let a_sem = Version::parse(a); + let b_sem = Version::parse(b); + + match (a_sem, b_sem) { + (Ok(av), Ok(bv)) => av.cmp(&bv), + _ => { + // Fallback for non-semver strings (e.g., "8.2") + let a_parts: Vec = a.split('.').filter_map(|s| s.parse().ok()).collect(); + let b_parts: Vec = b.split('.').filter_map(|s| s.parse().ok()).collect(); + a_parts.cmp(&b_parts) + } + } + }); +} diff --git a/tests/cli.rs b/tests/cli.rs index d2f5e2c..f7edb90 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -47,6 +47,7 @@ fn test_env_bash() { cmd.arg("env").arg("--shell=bash"); cmd.assert() .success() + .stdout(predicate::str::contains("export PVM_DIR=")) .stdout(predicate::str::contains("export PATH=")); }