mirror of
https://github.com/zhom/banderole.git
synced 2026-06-06 14:33:53 +02:00
feat: show progress bar and clearly separate steps
This commit is contained in:
Vendored
+2
@@ -5,7 +5,9 @@
|
||||
"dtolnay",
|
||||
"enabledelayedexpansion",
|
||||
"ERRORLEVEL",
|
||||
"flate",
|
||||
"imagename",
|
||||
"indicatif",
|
||||
"LOCALAPPDATA",
|
||||
"lockfiles",
|
||||
"mktemp",
|
||||
|
||||
Generated
+251
-45
@@ -28,6 +28,15 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@@ -45,9 +54,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.19"
|
||||
version = "0.6.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
|
||||
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -75,22 +84,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
|
||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.9"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
|
||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -143,14 +152,21 @@ dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"clap",
|
||||
"console",
|
||||
"directories",
|
||||
"env_logger",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"indicatif",
|
||||
"indicatif-log-bridge",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"sha2",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"uuid",
|
||||
@@ -202,9 +218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.30"
|
||||
version = "1.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
|
||||
checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@@ -244,9 +260,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.41"
|
||||
version = "4.5.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
|
||||
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -254,9 +270,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.41"
|
||||
version = "4.5.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
|
||||
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -288,6 +304,19 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
@@ -407,6 +436,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
@@ -416,6 +451,29 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -438,6 +496,18 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.2"
|
||||
@@ -609,9 +679,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785"
|
||||
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -628,9 +698,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -906,6 +976,29 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
|
||||
dependencies = [
|
||||
"console",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"unit-prefix",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif-log-bridge"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63703cf9069b85dbe6fe26e1c5230d013dee99d3559cd3d02ba39e099ef7ab02"
|
||||
dependencies = [
|
||||
"indicatif",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -954,6 +1047,30 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.33"
|
||||
@@ -982,9 +1099,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libbz2-rs-sys"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "775bf80d5878ab7c2b1080b5351a48b2f737d9f6f8b383574eebcc22be0dfccb"
|
||||
checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
@@ -1014,12 +1131,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.6"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1251,6 +1369,21 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
@@ -1298,24 +1431,53 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.16"
|
||||
version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792"
|
||||
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.0"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.22"
|
||||
@@ -1395,9 +1557,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.30"
|
||||
version = "0.23.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "069a8df149a16b1a12dcc31497c3396a173844be3cac4bd40c9e7671fef96671"
|
||||
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
@@ -1428,9 +1590,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -1522,9 +1684,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.141"
|
||||
version = "1.0.142"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
|
||||
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -1599,9 +1761,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
|
||||
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -1614,9 +1776,9 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.10"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
@@ -1704,6 +1866,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.20.0"
|
||||
@@ -1768,9 +1941,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.0"
|
||||
version = "1.47.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
|
||||
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -1819,9 +1992,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.15"
|
||||
version = "0.7.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
||||
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -1912,6 +2085,18 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
|
||||
|
||||
[[package]]
|
||||
name = "unit-prefix"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -2092,6 +2277,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
@@ -2195,7 +2390,7 @@ version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.2",
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2216,10 +2411,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.2"
|
||||
version = "0.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
|
||||
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
@@ -2341,6 +2537,16 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
@@ -2419,9 +2625,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.2"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
||||
@@ -25,6 +25,13 @@ futures-util = "0.3"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tempfile = "3.20"
|
||||
base64 = "0.22"
|
||||
indicatif = "0.18"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
indicatif-log-bridge = "0.2"
|
||||
console = "0.16"
|
||||
flate2 = "1.0"
|
||||
tar = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
|
||||
+275
-37
@@ -3,10 +3,15 @@ use crate::node_downloader::NodeDownloader;
|
||||
use crate::node_version_manager::NodeVersionManager;
|
||||
use crate::platform::Platform;
|
||||
use anyhow::{Context, Result};
|
||||
use console::{style, Emoji};
|
||||
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
|
||||
use log::{debug, info, warn};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
use zip::ZipWriter;
|
||||
|
||||
/// Public entry-point used by `main.rs`.
|
||||
@@ -24,6 +29,7 @@ pub async fn bundle_project(
|
||||
custom_name: Option<String>,
|
||||
no_compression: bool,
|
||||
ignore_cached_versions: bool,
|
||||
multi: &MultiProgress,
|
||||
) -> Result<()> {
|
||||
let project_path = project_path
|
||||
.canonicalize()
|
||||
@@ -54,24 +60,62 @@ pub async fn bundle_project(
|
||||
.await
|
||||
.unwrap_or_else(|_| "22.17.1".into());
|
||||
|
||||
println!(
|
||||
"Bundling {app_name} v{app_version} using Node.js v{node_version} for {plat}",
|
||||
info!(
|
||||
"Preparing build for {app_name} v{app_version} (Node {node_version}, {plat})",
|
||||
plat = Platform::current()
|
||||
);
|
||||
|
||||
if source_dir != project_path {
|
||||
println!("Using source directory: {}", source_dir.display());
|
||||
debug!("Using source directory: {}", source_dir.display());
|
||||
}
|
||||
|
||||
let output_path = resolve_output_path(output_path, &app_name, custom_name.as_deref())?;
|
||||
|
||||
// Styles
|
||||
let spinner_style =
|
||||
ProgressStyle::with_template("{prefix:.bold.dim} {spinner:.green} {wide_msg}")
|
||||
.unwrap()
|
||||
.tick_chars("/|\\- ");
|
||||
let bar_style = ProgressStyle::with_template(
|
||||
"{prefix:.bold.dim} {msg}[ {wide_bar} ] {pos}/{len}",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("#>-");
|
||||
|
||||
let emoji_prepare = Emoji("🔧", "");
|
||||
let emoji_bundle = Emoji("📦", "");
|
||||
let emoji_build = Emoji("⚙️ ", "");
|
||||
let emoji_done = Emoji("✨ ", "");
|
||||
let started = Instant::now();
|
||||
|
||||
// Stage 1: Prepare environment (resolve version + Node ready)
|
||||
println!(
|
||||
"{} {} Preparing environment...",
|
||||
style("[1/3]").bold().dim(),
|
||||
emoji_prepare
|
||||
);
|
||||
let pb_prepare = multi.add(ProgressBar::new_spinner());
|
||||
pb_prepare.set_style(spinner_style.clone());
|
||||
|
||||
let node_downloader = NodeDownloader::new_with_persistent_cache(&node_version).await?;
|
||||
let node_executable = node_downloader.ensure_node_binary().await?;
|
||||
let node_executable = node_downloader
|
||||
.ensure_node_binary_with_progress(Some(&pb_prepare))
|
||||
.await?;
|
||||
let node_root = node_executable
|
||||
.parent()
|
||||
.expect("node executable must have a parent")
|
||||
.parent()
|
||||
.unwrap_or_else(|| panic!("Unexpected node layout for {}", node_executable.display()));
|
||||
pb_prepare.finish_and_clear();
|
||||
|
||||
// Stage 2: Bundle application into archive
|
||||
println!(
|
||||
"{} {} Bundling application...",
|
||||
style("[2/3]").bold().dim(),
|
||||
emoji_bundle
|
||||
);
|
||||
let pb_bundle = multi.add(ProgressBar::new(0));
|
||||
pb_bundle.set_style(bar_style.clone());
|
||||
|
||||
let mut zip_data: Vec<u8> = Vec::new();
|
||||
{
|
||||
@@ -84,20 +128,98 @@ pub async fn bundle_project(
|
||||
.compression_level(Some(8))
|
||||
};
|
||||
|
||||
add_dir_to_zip_excluding_node_modules(&mut zip, &source_dir, Path::new("app"), opts)?;
|
||||
// Pre-count app files
|
||||
let app_files = count_files_in_dir(&source_dir, true, true);
|
||||
pb_bundle.set_length(app_files);
|
||||
add_dir_to_zip_excluding_node_modules(
|
||||
&mut zip,
|
||||
&source_dir,
|
||||
Path::new("app"),
|
||||
opts,
|
||||
Some(&pb_bundle),
|
||||
)?;
|
||||
|
||||
bundle_dependencies(&mut zip, &project_path, &source_dir, &package_value, opts)?;
|
||||
// Dependencies will extend the total as we discover them
|
||||
bundle_dependencies(
|
||||
&mut zip,
|
||||
&project_path,
|
||||
&source_dir,
|
||||
&package_value,
|
||||
opts,
|
||||
Some(&pb_bundle),
|
||||
)?;
|
||||
|
||||
add_dir_to_zip(&mut zip, node_root, Path::new("node"), opts)?;
|
||||
// Count node runtime files and extend length
|
||||
let node_files = count_files_in_dir(node_root, false, true);
|
||||
let new_len = pb_bundle.length().unwrap_or(0) + node_files;
|
||||
pb_bundle.set_length(new_len);
|
||||
add_dir_to_zip(
|
||||
&mut zip,
|
||||
node_root,
|
||||
Path::new("node"),
|
||||
opts,
|
||||
Some(&pb_bundle),
|
||||
)?;
|
||||
zip.finish()?;
|
||||
}
|
||||
pb_bundle.finish_and_clear();
|
||||
|
||||
executable::create_self_extracting_executable(&output_path, zip_data, &app_name)?;
|
||||
// Stage 3: Create executable
|
||||
println!(
|
||||
"{} {} Building native binary...",
|
||||
style("[3/3]").bold().dim(),
|
||||
emoji_build
|
||||
);
|
||||
let pb_build = multi.add(ProgressBar::new(0));
|
||||
// Do not show a determinate bar yet; use a spinner until total is known
|
||||
pb_build.set_style(spinner_style.clone());
|
||||
|
||||
println!("Bundle created at {}", output_path.display());
|
||||
executable::create_self_extracting_executable_with_progress(
|
||||
&output_path,
|
||||
zip_data,
|
||||
&app_name,
|
||||
Some(&pb_build),
|
||||
)?;
|
||||
pb_build.finish_and_clear();
|
||||
|
||||
println!(
|
||||
"{} Done in {}",
|
||||
emoji_done,
|
||||
HumanDuration(started.elapsed())
|
||||
);
|
||||
|
||||
info!("Bundle created at {}", output_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Count files (and symlinks) in a directory. Optionally exclude top-level node_modules.
|
||||
fn count_files_in_dir(dir: &Path, exclude_node_modules: bool, follow_links: bool) -> u64 {
|
||||
let mut count = 0u64;
|
||||
let walker = if follow_links {
|
||||
walkdir::WalkDir::new(dir).follow_links(true)
|
||||
} else {
|
||||
walkdir::WalkDir::new(dir).follow_links(false)
|
||||
};
|
||||
for entry in walker.into_iter().flatten() {
|
||||
let path = entry.path();
|
||||
if exclude_node_modules {
|
||||
if let Ok(rel) = path.strip_prefix(dir) {
|
||||
if rel
|
||||
.components()
|
||||
.next()
|
||||
.is_some_and(|c| c.as_os_str() == "node_modules")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if entry.file_type().is_file() || entry.file_type().is_symlink() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// Bundle dependencies with improved package manager support
|
||||
fn bundle_dependencies<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
@@ -105,6 +227,7 @@ fn bundle_dependencies<W>(
|
||||
source_dir: &Path,
|
||||
_package_value: &Value,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -133,16 +256,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let deps_result = find_and_bundle_dependencies(zip, project_path, opts)?;
|
||||
let deps_result = find_and_bundle_dependencies(zip, project_path, opts, progress)?;
|
||||
|
||||
if deps_result.dependencies_found {
|
||||
println!("Bundled dependencies: {}", deps_result.source_description);
|
||||
debug!("Bundled dependencies: {}", deps_result.source_description);
|
||||
} else {
|
||||
println!("Warning: No dependencies found to bundle");
|
||||
debug!("No dependencies found to bundle");
|
||||
}
|
||||
|
||||
for warning in &deps_result.warnings {
|
||||
println!("Warning: {warning}");
|
||||
debug!("{warning}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -159,6 +282,7 @@ fn find_and_bundle_dependencies<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<DependenciesResult>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -194,7 +318,7 @@ where
|
||||
if !is_pnpm_workspace {
|
||||
match package_manager {
|
||||
PackageManager::Pnpm => {
|
||||
bundle_pnpm_dependencies(zip, project_path, opts)?;
|
||||
bundle_pnpm_dependencies(zip, project_path, opts, progress)?;
|
||||
return Ok(DependenciesResult {
|
||||
dependencies_found: true,
|
||||
source_description: "pnpm dependencies (node_modules + .pnpm)".to_string(),
|
||||
@@ -207,6 +331,7 @@ where
|
||||
&project_node_modules,
|
||||
project_path,
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
return Ok(DependenciesResult {
|
||||
dependencies_found: true,
|
||||
@@ -220,6 +345,7 @@ where
|
||||
&project_node_modules,
|
||||
project_path,
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
return Ok(DependenciesResult {
|
||||
dependencies_found: true,
|
||||
@@ -263,7 +389,13 @@ where
|
||||
|
||||
match package_manager {
|
||||
PackageManager::Pnpm => {
|
||||
bundle_pnpm_workspace_dependencies(zip, parent_path, project_path, opts)?;
|
||||
bundle_pnpm_workspace_dependencies(
|
||||
zip,
|
||||
parent_path,
|
||||
project_path,
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
return Ok(DependenciesResult {
|
||||
dependencies_found: true,
|
||||
source_description: format!(
|
||||
@@ -280,6 +412,7 @@ where
|
||||
parent_path,
|
||||
project_path,
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
return Ok(DependenciesResult {
|
||||
dependencies_found: true,
|
||||
@@ -357,6 +490,7 @@ fn bundle_pnpm_dependencies<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -366,7 +500,18 @@ where
|
||||
|
||||
if !pnpm_dir.exists() {
|
||||
if node_modules_path.exists() {
|
||||
add_dir_to_zip_no_follow(zip, &node_modules_path, Path::new("app/node_modules"), opts)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.set_length(
|
||||
pb.length().unwrap_or(0) + count_files_in_dir(&node_modules_path, false, false),
|
||||
);
|
||||
}
|
||||
add_dir_to_zip_no_follow(
|
||||
zip,
|
||||
&node_modules_path,
|
||||
Path::new("app/node_modules"),
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -397,7 +542,7 @@ where
|
||||
)?;
|
||||
}
|
||||
|
||||
println!(
|
||||
debug!(
|
||||
"Bundling {} packages (resolved dependencies) for pnpm project",
|
||||
resolved_packages.len()
|
||||
);
|
||||
@@ -405,16 +550,30 @@ where
|
||||
zip.add_directory("app/node_modules/", opts)?;
|
||||
|
||||
for package_name in &resolved_packages {
|
||||
if let Err(e) =
|
||||
copy_pnpm_package_comprehensive(zip, &node_modules_path, &pnpm_dir, package_name, opts)
|
||||
{
|
||||
println!("Warning: Failed to copy package {package_name}: {e}");
|
||||
if let Err(e) = copy_pnpm_package_comprehensive(
|
||||
zip,
|
||||
&node_modules_path,
|
||||
&pnpm_dir,
|
||||
package_name,
|
||||
opts,
|
||||
progress,
|
||||
) {
|
||||
warn!("Failed to copy package {package_name}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let bin_dir = node_modules_path.join(".bin");
|
||||
if bin_dir.exists() {
|
||||
add_dir_to_zip_no_follow(zip, &bin_dir, Path::new("app/node_modules/.bin"), opts)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.set_length(pb.length().unwrap_or(0) + count_files_in_dir(&bin_dir, false, false));
|
||||
}
|
||||
add_dir_to_zip_no_follow(
|
||||
zip,
|
||||
&bin_dir,
|
||||
Path::new("app/node_modules/.bin"),
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
}
|
||||
|
||||
let important_files = [".modules.yaml", ".pnpm-workspace-state-v1.json"];
|
||||
@@ -594,6 +753,7 @@ fn copy_pnpm_package_comprehensive<W>(
|
||||
pnpm_dir: &Path,
|
||||
package_name: &str,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -618,7 +778,12 @@ where
|
||||
};
|
||||
|
||||
if target_path.exists() {
|
||||
add_dir_to_zip_no_follow_skip_parents(zip, &target_path, &dest_path, opts)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.set_length(
|
||||
pb.length().unwrap_or(0) + count_files_in_dir(&target_path, false, false),
|
||||
);
|
||||
}
|
||||
add_dir_to_zip_no_follow_skip_parents(zip, &target_path, &dest_path, opts, progress)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -629,11 +794,18 @@ where
|
||||
if extracted_name == package_name {
|
||||
let pnpm_package_path = entry.path().join("node_modules").join(package_name);
|
||||
if pnpm_package_path.exists() {
|
||||
if let Some(pb) = progress {
|
||||
pb.set_length(
|
||||
pb.length().unwrap_or(0)
|
||||
+ count_files_in_dir(&pnpm_package_path, false, false),
|
||||
);
|
||||
}
|
||||
add_dir_to_zip_no_follow_skip_parents(
|
||||
zip,
|
||||
&pnpm_package_path,
|
||||
&dest_path,
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -650,6 +822,7 @@ fn bundle_node_modules_comprehensive<W>(
|
||||
node_modules_path: &Path,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -690,7 +863,7 @@ where
|
||||
)?;
|
||||
}
|
||||
|
||||
println!(
|
||||
debug!(
|
||||
"Bundling {} packages (resolved dependencies) for pnpm node_modules",
|
||||
resolved_packages.len()
|
||||
);
|
||||
@@ -704,8 +877,9 @@ where
|
||||
&pnpm_dir,
|
||||
package_name,
|
||||
opts,
|
||||
progress,
|
||||
) {
|
||||
println!("Warning: Failed to copy package {package_name}: {e}");
|
||||
warn!("Failed to copy package {package_name}: {e}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -719,7 +893,7 @@ where
|
||||
)?;
|
||||
}
|
||||
|
||||
println!(
|
||||
debug!(
|
||||
"Bundling {} packages (resolved dependencies) for regular node_modules",
|
||||
resolved_packages.len()
|
||||
);
|
||||
@@ -727,15 +901,26 @@ where
|
||||
zip.add_directory("app/node_modules/", opts)?;
|
||||
|
||||
for package_name in &resolved_packages {
|
||||
if let Err(e) = copy_workspace_package(zip, node_modules_path, package_name, opts) {
|
||||
println!("Warning: Failed to copy package {package_name}: {e}");
|
||||
if let Err(e) =
|
||||
copy_workspace_package(zip, node_modules_path, package_name, opts, progress)
|
||||
{
|
||||
warn!("Failed to copy package {package_name}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let bin_dir = node_modules_path.join(".bin");
|
||||
if bin_dir.exists() {
|
||||
add_dir_to_zip_no_follow(zip, &bin_dir, Path::new("app/node_modules/.bin"), opts)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.set_length(pb.length().unwrap_or(0) + count_files_in_dir(&bin_dir, false, false));
|
||||
}
|
||||
add_dir_to_zip_no_follow(
|
||||
zip,
|
||||
&bin_dir,
|
||||
Path::new("app/node_modules/.bin"),
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
}
|
||||
|
||||
let important_files = [".modules.yaml", ".pnpm-workspace-state-v1.json"];
|
||||
@@ -746,6 +931,9 @@ where
|
||||
zip.start_file(dest_path.to_string_lossy().as_ref(), opts)?;
|
||||
let data = fs::read(&file_path)?;
|
||||
zip.write_all(&data)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -759,6 +947,7 @@ fn bundle_workspace_dependencies<W>(
|
||||
_parent_path: &Path,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -796,7 +985,7 @@ where
|
||||
)?;
|
||||
}
|
||||
|
||||
println!(
|
||||
debug!(
|
||||
"Bundling {} packages (resolved dependencies) for workspace node_modules",
|
||||
resolved_packages.len()
|
||||
);
|
||||
@@ -804,14 +993,24 @@ where
|
||||
zip.add_directory("app/node_modules/", opts)?;
|
||||
|
||||
for package_name in &resolved_packages {
|
||||
if let Err(e) = copy_workspace_package(zip, node_modules_path, package_name, opts) {
|
||||
println!("Warning: Failed to copy package {package_name}: {e}");
|
||||
if let Err(e) = copy_workspace_package(zip, node_modules_path, package_name, opts, progress)
|
||||
{
|
||||
warn!("Failed to copy package {package_name}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let bin_dir = node_modules_path.join(".bin");
|
||||
if bin_dir.exists() {
|
||||
add_dir_to_zip_no_follow(zip, &bin_dir, Path::new("app/node_modules/.bin"), opts)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.set_length(pb.length().unwrap_or(0) + count_files_in_dir(&bin_dir, false, false));
|
||||
}
|
||||
add_dir_to_zip_no_follow(
|
||||
zip,
|
||||
&bin_dir,
|
||||
Path::new("app/node_modules/.bin"),
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
}
|
||||
|
||||
let important_files = [".modules.yaml"];
|
||||
@@ -822,6 +1021,9 @@ where
|
||||
zip.start_file(dest_path.to_string_lossy().as_ref(), opts)?;
|
||||
let data = fs::read(&file_path)?;
|
||||
zip.write_all(&data)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,6 +1036,7 @@ fn bundle_pnpm_workspace_dependencies<W>(
|
||||
parent_path: &Path,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -872,7 +1075,7 @@ where
|
||||
)?;
|
||||
}
|
||||
|
||||
println!(
|
||||
debug!(
|
||||
"Bundling {} packages (resolved dependencies) for workspace pnpm node_modules",
|
||||
resolved_packages.len()
|
||||
);
|
||||
@@ -887,14 +1090,24 @@ where
|
||||
&parent_path.join("node_modules").join(".pnpm"),
|
||||
package_name,
|
||||
opts,
|
||||
progress,
|
||||
) {
|
||||
println!("Warning: Failed to copy package {package_name}: {e}");
|
||||
warn!("Failed to copy package {package_name}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let bin_dir = parent_path.join("node_modules").join(".bin");
|
||||
if bin_dir.exists() {
|
||||
add_dir_to_zip_no_follow(zip, &bin_dir, Path::new("app/node_modules/.bin"), opts)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.set_length(pb.length().unwrap_or(0) + count_files_in_dir(&bin_dir, false, false));
|
||||
}
|
||||
add_dir_to_zip_no_follow(
|
||||
zip,
|
||||
&bin_dir,
|
||||
Path::new("app/node_modules/.bin"),
|
||||
opts,
|
||||
progress,
|
||||
)?;
|
||||
}
|
||||
|
||||
let important_files = [".modules.yaml", ".pnpm-workspace-state-v1.json"];
|
||||
@@ -905,6 +1118,9 @@ where
|
||||
zip.start_file(dest_path.to_string_lossy().as_ref(), opts)?;
|
||||
let data = fs::read(&file_path)?;
|
||||
zip.write_all(&data)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,6 +1343,7 @@ fn add_dir_to_zip<W>(
|
||||
src_dir: &Path,
|
||||
dest_dir: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -1164,6 +1381,9 @@ where
|
||||
zip.start_file(zip_path.to_string_lossy().as_ref(), file_opts)?;
|
||||
let data = fs::read(path).context("Failed to read file while zipping")?;
|
||||
zip.write_all(&data)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1174,6 +1394,7 @@ fn add_dir_to_zip_no_follow<W>(
|
||||
src_dir: &Path,
|
||||
dest_dir: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -1219,6 +1440,9 @@ where
|
||||
let data = fs::read(path).context("Failed to read file while zipping")?;
|
||||
zip.write_all(&data)?;
|
||||
}
|
||||
if let Some(pb) = progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1229,6 +1453,7 @@ fn add_dir_to_zip_no_follow_skip_parents<W>(
|
||||
src_dir: &Path,
|
||||
dest_dir: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -1276,6 +1501,9 @@ where
|
||||
let data = fs::read(path).context("Failed to read file while zipping")?;
|
||||
zip.write_all(&data)?;
|
||||
}
|
||||
if let Some(pb) = progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1286,6 +1514,7 @@ fn add_dir_to_zip_excluding_node_modules<W>(
|
||||
src_dir: &Path,
|
||||
dest_dir: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -1327,6 +1556,9 @@ where
|
||||
zip.start_file(zip_path.to_string_lossy().as_ref(), file_opts)?;
|
||||
let data = fs::read(path).context("Failed to read file while zipping")?;
|
||||
zip.write_all(&data)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1337,6 +1569,7 @@ fn copy_workspace_package<W>(
|
||||
node_modules_path: &Path,
|
||||
package_name: &str,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
@@ -1358,7 +1591,12 @@ where
|
||||
};
|
||||
|
||||
if target_path.exists() {
|
||||
add_dir_to_zip_no_follow_skip_parents(zip, &target_path, &dest_path, opts)?;
|
||||
if let Some(pb) = progress {
|
||||
pb.set_length(
|
||||
pb.length().unwrap_or(0) + count_files_in_dir(&target_path, false, false),
|
||||
);
|
||||
}
|
||||
add_dir_to_zip_no_follow_skip_parents(zip, &target_path, &dest_path, opts, progress)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
+412
-18
@@ -1,4 +1,6 @@
|
||||
use anyhow::{Context, Result};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::{error, info};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
@@ -9,40 +11,37 @@ use crate::embedded_template::EmbeddedTemplate;
|
||||
use crate::platform::Platform;
|
||||
use crate::rust_toolchain::RustToolchain;
|
||||
|
||||
/// Create a cross-platform Rust executable with embedded data
|
||||
pub fn create_self_extracting_executable(
|
||||
/// Create a cross-platform Rust executable with embedded data while reporting progress to the provided ProgressBar if any0
|
||||
pub fn create_self_extracting_executable_with_progress(
|
||||
output_path: &Path,
|
||||
zip_data: Vec<u8>,
|
||||
app_name: &str,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()> {
|
||||
// Check if Rust toolchain is available
|
||||
if let Err(e) = RustToolchain::check_availability() {
|
||||
eprintln!("\nError: {e}");
|
||||
eprintln!("{}", RustToolchain::get_installation_instructions());
|
||||
error!("\nError: {e}");
|
||||
error!("{}", RustToolchain::get_installation_instructions());
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let build_id = Uuid::new_v4().to_string();
|
||||
|
||||
// Create temporary directory for building
|
||||
let temp_dir = TempDir::new().context("Failed to create temporary directory")?;
|
||||
let build_dir = temp_dir.path();
|
||||
|
||||
// Copy template to build directory
|
||||
copy_template_to_build_dir(build_dir)?;
|
||||
|
||||
// Write embedded data
|
||||
let zip_path = build_dir.join("embedded_data.zip");
|
||||
fs::write(&zip_path, &zip_data).context("Failed to write embedded zip data")?;
|
||||
|
||||
let build_id_path = build_dir.join("build_id.txt");
|
||||
fs::write(&build_id_path, &build_id).context("Failed to write build ID")?;
|
||||
|
||||
// Update Cargo.toml with app name
|
||||
update_cargo_toml(build_dir, app_name)?;
|
||||
|
||||
// Build the executable
|
||||
build_executable(build_dir, output_path, app_name)?;
|
||||
info!("Building native binary...");
|
||||
build_executable_with_progress(build_dir, output_path, app_name, progress)?;
|
||||
info!("Native binary built");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -88,23 +87,263 @@ fn sanitize_package_name(name: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn build_executable(build_dir: &Path, output_path: &Path, app_name: &str) -> Result<()> {
|
||||
fn build_executable_with_progress(
|
||||
build_dir: &Path,
|
||||
output_path: &Path,
|
||||
app_name: &str,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()> {
|
||||
let current_platform = Platform::current();
|
||||
let target_triple = get_target_triple(¤t_platform);
|
||||
|
||||
// Ensure we have the target installed
|
||||
install_rust_target(&target_triple)?;
|
||||
|
||||
// Build the executable
|
||||
// Do not show a determinate bar until we know the total
|
||||
|
||||
// Actual build; consume Cargo JSON messages to compute progress without a dry-run
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.current_dir(build_dir)
|
||||
.args(["build", "--release", "--target", &target_triple]);
|
||||
.args([
|
||||
"build",
|
||||
"--release",
|
||||
"--target",
|
||||
&target_triple,
|
||||
"--message-format",
|
||||
"json",
|
||||
])
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
|
||||
let output = cmd.output().context("Failed to execute cargo build")?;
|
||||
let mut child = cmd.spawn().context("Failed to execute cargo build")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("Cargo build failed:\n{}", stderr);
|
||||
// Capture stdout/stderr for diagnostics; parse JSON on stdout for compiled count
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
let stdout_buf: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
|
||||
let stderr_buf: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
|
||||
|
||||
// Spawn stdout reader + JSON progress parser
|
||||
let stdout_arc = Arc::clone(&stdout_buf);
|
||||
let pb_for_stdout = progress.cloned();
|
||||
let compiled_count = Arc::new(AtomicU64::new(0));
|
||||
let compiled_for_stdout = Arc::clone(&compiled_count);
|
||||
// Determine total crates using cargo metadata (no dry run, no stderr parsing)
|
||||
// Determine total first, before spawning cargo; don't show bar until known
|
||||
let known_total: u64 = compute_total_via_cargo_metadata(build_dir, &target_triple).unwrap_or(0);
|
||||
// Determine total compile units using cargo metadata; only then show a determinate bar
|
||||
if let Some(pb) = progress {
|
||||
if known_total > 0 {
|
||||
pb.set_style(
|
||||
ProgressStyle::with_template(
|
||||
"[ {wide_bar} ] {pos}/{len}",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("#>-"),
|
||||
);
|
||||
pb.set_length(known_total);
|
||||
pb.set_position(0);
|
||||
}
|
||||
}
|
||||
|
||||
let stdout_handle = child.stdout.take().map(|stdout| {
|
||||
std::thread::spawn(move || {
|
||||
use std::io::{BufRead, BufReader};
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut total_artifacts: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
let mut compiled_artifacts: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
for line in reader.lines() {
|
||||
let line = match line {
|
||||
Ok(l) => l,
|
||||
Err(_) => break,
|
||||
};
|
||||
if let Ok(mut buf) = stdout_arc.lock() {
|
||||
buf.push_str(&line);
|
||||
buf.push('\n');
|
||||
}
|
||||
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&line) {
|
||||
if let Some(reason) = value.get("reason").and_then(|r| r.as_str()) {
|
||||
if reason == "compiler-artifact" {
|
||||
let pkg = value
|
||||
.get("package_id")
|
||||
.and_then(|p| p.as_str())
|
||||
.unwrap_or("");
|
||||
let target_name = value
|
||||
.get("target")
|
||||
.and_then(|t| t.get("name"))
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("");
|
||||
let key = format!("{pkg}:{target_name}");
|
||||
total_artifacts.insert(key.clone());
|
||||
let is_fresh = value
|
||||
.get("fresh")
|
||||
.and_then(|f| f.as_bool())
|
||||
.unwrap_or(false);
|
||||
if !is_fresh {
|
||||
compiled_artifacts.insert(key);
|
||||
}
|
||||
|
||||
// Update compiled counter and progress position
|
||||
let compiled_now = compiled_artifacts.len() as u64;
|
||||
compiled_for_stdout.store(compiled_now, Ordering::SeqCst);
|
||||
if let Some(pb) = &pb_for_stdout {
|
||||
let total_len = known_total;
|
||||
if total_len > 0 && pb.length().unwrap_or(0) != total_len {
|
||||
pb.set_length(total_len);
|
||||
}
|
||||
let pos = if total_len > 0 {
|
||||
compiled_now.min(total_len)
|
||||
} else {
|
||||
compiled_now
|
||||
};
|
||||
pb.set_position(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Spawn stderr reader (diagnostics only)
|
||||
let stderr_arc = Arc::clone(&stderr_buf);
|
||||
let stderr_handle = child.stderr.take().map(|stderr| {
|
||||
std::thread::spawn(move || {
|
||||
use std::io::Read;
|
||||
let mut reader = std::io::BufReader::new(stderr);
|
||||
let mut capture_bytes: Vec<u8> = Vec::new();
|
||||
let _ = reader.read_to_end(&mut capture_bytes);
|
||||
if let Ok(mut buf) = stderr_arc.lock() {
|
||||
match String::from_utf8(capture_bytes) {
|
||||
Ok(s) => buf.push_str(&s),
|
||||
Err(_) => buf.push_str("<non-utf8 stderr>"),
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Consume JSON messages from stdout; estimate total as number of artifacts and compiled as non-fresh artifacts
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
use std::io::{BufRead, BufReader};
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut total_artifacts: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
let mut compiled_artifacts: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
for line in reader.lines() {
|
||||
let line = match line {
|
||||
Ok(l) => l,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Ok(value) = serde_json::from_str::<serde_json::Value>(&line) else {
|
||||
continue;
|
||||
};
|
||||
let Some(reason) = value.get("reason").and_then(|r| r.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
match reason {
|
||||
"compiler-artifact" => {
|
||||
let pkg = value
|
||||
.get("package_id")
|
||||
.and_then(|p| p.as_str())
|
||||
.unwrap_or("");
|
||||
let tname = value
|
||||
.get("target")
|
||||
.and_then(|t| t.get("name"))
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("");
|
||||
let key = format!("{pkg}:{tname}");
|
||||
total_artifacts.insert(key.clone());
|
||||
let is_fresh = value
|
||||
.get("fresh")
|
||||
.and_then(|f| f.as_bool())
|
||||
.unwrap_or(false);
|
||||
if !is_fresh {
|
||||
compiled_artifacts.insert(key);
|
||||
}
|
||||
|
||||
if let Some(pb) = progress {
|
||||
let total_now = total_artifacts.len() as u64;
|
||||
if total_now > 0 && pb.length().unwrap_or(0) != total_now {
|
||||
pb.set_length(total_now);
|
||||
}
|
||||
let compiled_now = compiled_artifacts.len() as u64;
|
||||
if compiled_now <= pb.length().unwrap_or(0) {
|
||||
pb.set_position(compiled_now);
|
||||
}
|
||||
|
||||
if let Some(name) = value
|
||||
.get("target")
|
||||
.and_then(|t| t.get("name"))
|
||||
.and_then(|n| n.as_str())
|
||||
{
|
||||
let len = pb.length().unwrap_or(0);
|
||||
let pos = pb.position();
|
||||
if len > 1 {
|
||||
pb.set_message(format!("Compiling binary: {name} ({pos}/{len})"));
|
||||
} else {
|
||||
pb.set_message(format!("Compiling binary: {name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"build-finished" => {
|
||||
if let Some(pb) = progress {
|
||||
if let Some(len) = pb.length() {
|
||||
if len > 0 {
|
||||
pb.set_position(len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status = child.wait().context("Failed to wait for cargo build")?;
|
||||
if let Some(h) = stdout_handle {
|
||||
let _ = h.join();
|
||||
}
|
||||
if let Some(h) = stderr_handle {
|
||||
let _ = h.join();
|
||||
}
|
||||
if !status.success() {
|
||||
let out = stdout_buf
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|s| s.clone())
|
||||
.unwrap_or_default();
|
||||
let err = stderr_buf
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|s| s.clone())
|
||||
.unwrap_or_default();
|
||||
let trim_tail = |mut s: String| {
|
||||
const MAX: usize = 4000;
|
||||
if s.len() > MAX {
|
||||
s.split_off(s.len() - MAX)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if s.len() > MAX {
|
||||
s[s.len() - MAX..].to_string()
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let out_tail = trim_tail(out);
|
||||
let err_tail = trim_tail(err);
|
||||
anyhow::bail!(
|
||||
"Cargo build failed.\nLast stdout:\n{}\nLast stderr:\n{}",
|
||||
out_tail,
|
||||
err_tail
|
||||
);
|
||||
}
|
||||
|
||||
// Get the sanitized package name to find the correct executable
|
||||
@@ -148,6 +387,161 @@ fn build_executable(build_dir: &Path, output_path: &Path, app_name: &str) -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_total_via_cargo_metadata(build_dir: &Path, target_triple: &str) -> Result<u64> {
|
||||
// Strategy: union of host + target resolve nodes, then count compile-relevant targets per package
|
||||
// Relevant targets: lib, proc-macro, custom-build for all packages; bin only for the root package
|
||||
|
||||
fn run_metadata(build_dir: &Path, args: &[&str]) -> Result<serde_json::Value> {
|
||||
let output = Command::new("cargo")
|
||||
.current_dir(build_dir)
|
||||
.args(args)
|
||||
.output()
|
||||
.with_context(|| format!("Failed to run cargo {}", args.join(" ")))?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"cargo {} failed: {}",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let v: serde_json::Value = serde_json::from_slice(&output.stdout)
|
||||
.context("Failed to parse cargo metadata JSON")?;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn get_host_triple() -> Result<String> {
|
||||
let output = Command::new("rustc")
|
||||
.arg("-vV")
|
||||
.output()
|
||||
.context("Failed to run rustc -vV")?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"rustc -vV failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
if let Some(rest) = line.strip_prefix("host: ") {
|
||||
return Ok(rest.trim().to_string());
|
||||
}
|
||||
}
|
||||
anyhow::bail!("Failed to parse host triple from rustc -vV")
|
||||
}
|
||||
|
||||
// Run three metadata queries: target-filtered, host-filtered, and unfiltered for packages map
|
||||
let meta_target = run_metadata(
|
||||
build_dir,
|
||||
&[
|
||||
"metadata",
|
||||
"--format-version",
|
||||
"1",
|
||||
"--filter-platform",
|
||||
target_triple,
|
||||
],
|
||||
)?;
|
||||
let host_triple = get_host_triple().unwrap_or_else(|_| target_triple.to_string());
|
||||
let meta_host = run_metadata(
|
||||
build_dir,
|
||||
&[
|
||||
"metadata",
|
||||
"--format-version",
|
||||
"1",
|
||||
"--filter-platform",
|
||||
&host_triple,
|
||||
],
|
||||
)?;
|
||||
let meta_all = run_metadata(build_dir, &["metadata", "--format-version", "1"])?;
|
||||
|
||||
// Collect union of package ids to be considered
|
||||
let mut pkg_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let push_ids = |val: &serde_json::Value, set: &mut std::collections::HashSet<String>| {
|
||||
if let Some(nodes) = val
|
||||
.get("resolve")
|
||||
.and_then(|r| r.get("nodes"))
|
||||
.and_then(|n| n.as_array())
|
||||
{
|
||||
for node in nodes {
|
||||
if let Some(id) = node.get("id").and_then(|i| i.as_str()) {
|
||||
set.insert(id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
push_ids(&meta_target, &mut pkg_ids);
|
||||
push_ids(&meta_host, &mut pkg_ids);
|
||||
|
||||
// Build package map from unfiltered metadata
|
||||
let mut packages_by_id: std::collections::HashMap<String, serde_json::Value> =
|
||||
std::collections::HashMap::new();
|
||||
if let Some(packages) = meta_all.get("packages").and_then(|p| p.as_array()) {
|
||||
for p in packages {
|
||||
if let Some(id) = p.get("id").and_then(|i| i.as_str()) {
|
||||
packages_by_id.insert(id.to_string(), p.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Root package id
|
||||
let root_id = meta_target
|
||||
.get("resolve")
|
||||
.and_then(|r| r.get("root"))
|
||||
.and_then(|r| r.as_str())
|
||||
.or_else(|| {
|
||||
meta_all
|
||||
.get("resolve")
|
||||
.and_then(|r| r.get("root"))
|
||||
.and_then(|r| r.as_str())
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let mut total_units: u64 = 0;
|
||||
for pid in pkg_ids {
|
||||
let Some(pkg) = packages_by_id.get(&pid) else {
|
||||
continue;
|
||||
};
|
||||
let is_root = root_id.as_ref().is_some_and(|r| r == &pid);
|
||||
if let Some(targets) = pkg.get("targets").and_then(|t| t.as_array()) {
|
||||
for t in targets {
|
||||
let has_kind = |name: &str| -> bool {
|
||||
t.get("kind")
|
||||
.and_then(|k| k.as_array())
|
||||
.is_some_and(|kinds| kinds.iter().any(|v| v.as_str() == Some(name)))
|
||||
};
|
||||
if has_kind("custom-build") {
|
||||
total_units += 1;
|
||||
continue;
|
||||
}
|
||||
if has_kind("proc-macro") {
|
||||
total_units += 1;
|
||||
continue;
|
||||
}
|
||||
if has_kind("lib") {
|
||||
total_units += 1;
|
||||
continue;
|
||||
}
|
||||
if is_root && has_kind("bin") {
|
||||
total_units += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if total_units == 0 {
|
||||
// Fallback to node counts if our logic fails
|
||||
let nodes_len = meta_target
|
||||
.get("resolve")
|
||||
.and_then(|r| r.get("nodes"))
|
||||
.and_then(|n| n.as_array())
|
||||
.map(|a| a.len() as u64)
|
||||
.unwrap_or(1);
|
||||
return Ok(nodes_len.max(1));
|
||||
}
|
||||
|
||||
Ok(total_units)
|
||||
}
|
||||
|
||||
fn get_target_triple(platform: &Platform) -> String {
|
||||
match platform {
|
||||
Platform::MacosX64 => "x86_64-apple-darwin".to_string(),
|
||||
|
||||
+25
-2
@@ -7,6 +7,9 @@ mod platform;
|
||||
mod rust_toolchain;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use indicatif::MultiProgress;
|
||||
use indicatif_log_bridge::LogWrapper;
|
||||
use log::LevelFilter;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -17,6 +20,9 @@ use std::path::PathBuf;
|
||||
long_about = "Banderole packages Node.js applications with portable Node binaries into a single binary for easy distribution and execution"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Enable verbose output
|
||||
#[arg(short, long, global = true)]
|
||||
verbose: bool,
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
@@ -44,8 +50,18 @@ enum Commands {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Initialize env_logger wrapped by indicatif's log bridge so logs play nice with progress bars
|
||||
let multi_progress = MultiProgress::new();
|
||||
let cli = Cli::parse();
|
||||
|
||||
let default_level = if cli.verbose { "debug" } else { "warn" };
|
||||
let built_logger =
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_level))
|
||||
.build();
|
||||
let level: LevelFilter = built_logger.filter();
|
||||
LogWrapper::new(multi_progress.clone(), built_logger).try_init()?;
|
||||
log::set_max_level(level);
|
||||
|
||||
match cli.command {
|
||||
Commands::Bundle {
|
||||
path,
|
||||
@@ -54,8 +70,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
no_compression,
|
||||
ignore_cached_versions,
|
||||
} => {
|
||||
bundler::bundle_project(path, output, name, no_compression, ignore_cached_versions)
|
||||
.await?;
|
||||
bundler::bundle_project(
|
||||
path,
|
||||
output,
|
||||
name,
|
||||
no_compression,
|
||||
ignore_cached_versions,
|
||||
&multi_progress,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+206
-69
@@ -2,7 +2,9 @@ use crate::node_version_manager::NodeVersionManager;
|
||||
use crate::platform::Platform;
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::StreamExt;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use lazy_static::lazy_static;
|
||||
use log::info;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
@@ -25,14 +27,17 @@ impl NodeDownloader {
|
||||
let version_resolver = NodeVersionManager::new();
|
||||
|
||||
// Resolve the version specification to a concrete version
|
||||
let resolved_version = version_resolver
|
||||
.resolve_version(version_spec, false)
|
||||
.await
|
||||
.context(format!(
|
||||
"Failed to resolve Node.js version '{version_spec}'"
|
||||
))?;
|
||||
let resolved_version = match parse_full_version_spec(version_spec) {
|
||||
Some(full) => full,
|
||||
None => version_resolver
|
||||
.resolve_version(version_spec, false)
|
||||
.await
|
||||
.context(format!(
|
||||
"Failed to resolve Node.js version '{version_spec}'"
|
||||
))?,
|
||||
};
|
||||
|
||||
println!("Resolved '{version_spec}' to Node.js version {resolved_version}");
|
||||
info!("Resolved '{version_spec}' to Node.js version {resolved_version}");
|
||||
|
||||
Ok(Self {
|
||||
platform: Platform::current(),
|
||||
@@ -58,7 +63,15 @@ impl NodeDownloader {
|
||||
Ok(cache_dir)
|
||||
}
|
||||
|
||||
pub async fn ensure_node_binary(&self) -> Result<PathBuf> {
|
||||
/// Same as ensure_node_binary but reports progress to the provided ProgressBar if any
|
||||
pub async fn ensure_node_binary_with_progress(
|
||||
&self,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<PathBuf> {
|
||||
self.ensure_node_binary_inner(progress).await
|
||||
}
|
||||
|
||||
async fn ensure_node_binary_inner(&self, progress: Option<&ProgressBar>) -> Result<PathBuf> {
|
||||
// Create cache key for this version and platform
|
||||
let cache_key = format!("{}:{}", self.node_version, self.platform);
|
||||
|
||||
@@ -92,8 +105,8 @@ impl NodeDownloader {
|
||||
return Ok(node_executable);
|
||||
}
|
||||
|
||||
println!(
|
||||
"Downloading Node.js {} for {}...",
|
||||
info!(
|
||||
"Fetching Node.js {} for {}",
|
||||
self.node_version, self.platform
|
||||
);
|
||||
|
||||
@@ -103,7 +116,7 @@ impl NodeDownloader {
|
||||
.context("Failed to create node cache directory")?;
|
||||
|
||||
// Download and extract Node.js
|
||||
self.download_and_extract_node(&node_dir).await?;
|
||||
self.download_and_extract_node(&node_dir, progress).await?;
|
||||
|
||||
if !node_executable.exists() {
|
||||
anyhow::bail!(
|
||||
@@ -124,7 +137,11 @@ impl NodeDownloader {
|
||||
Ok(node_executable)
|
||||
}
|
||||
|
||||
async fn download_and_extract_node(&self, target_dir: &Path) -> Result<()> {
|
||||
async fn download_and_extract_node(
|
||||
&self,
|
||||
target_dir: &Path,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()> {
|
||||
let archive_name = self.platform.node_archive_name(&self.node_version);
|
||||
let url = format!(
|
||||
"https://nodejs.org/dist/v{}/{}",
|
||||
@@ -145,22 +162,64 @@ impl NodeDownloader {
|
||||
.await
|
||||
.context("Failed to create archive file")?;
|
||||
|
||||
// Configure a download progress bar style like the indicatif example
|
||||
// Template inspired by download-speed.rs example
|
||||
if let (Some(pb), Some(total)) = (progress, response.content_length()) {
|
||||
pb.set_style(
|
||||
ProgressStyle::with_template(
|
||||
"[ {wide_bar} ] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("#>-"),
|
||||
);
|
||||
pb.set_length(total);
|
||||
} else if let Some(pb) = progress {
|
||||
pb.set_style(
|
||||
ProgressStyle::with_template(
|
||||
"[ {wide_bar} ] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
|
||||
)
|
||||
.unwrap()
|
||||
.tick_chars("/|\\- "),
|
||||
);
|
||||
}
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.context("Failed to read download chunk")?;
|
||||
file.write_all(&chunk)
|
||||
.await
|
||||
.context("Failed to write archive chunk")?;
|
||||
if let Some(pb) = progress {
|
||||
if pb.length().is_some() {
|
||||
pb.inc(chunk.len() as u64);
|
||||
} else {
|
||||
pb.tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file.flush().await.context("Failed to flush archive file")?;
|
||||
drop(file);
|
||||
|
||||
// Extract the archive
|
||||
// Extract the archive with determinate progress
|
||||
if let Some(pb) = progress {
|
||||
pb.set_style(
|
||||
ProgressStyle::with_template(
|
||||
"[ {wide_bar} ] {pos}/{len}",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("#>-"),
|
||||
);
|
||||
pb.set_length(0);
|
||||
pb.set_position(0);
|
||||
}
|
||||
if self.platform.is_windows() {
|
||||
self.extract_zip(&archive_path, target_dir).await?;
|
||||
self.extract_zip(&archive_path, target_dir, progress)
|
||||
.await?;
|
||||
} else {
|
||||
self.extract_tar_gz(&archive_path, target_dir).await?;
|
||||
self.extract_tar_gz(&archive_path, target_dir, progress)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Clean up archive
|
||||
@@ -178,78 +237,156 @@ impl NodeDownloader {
|
||||
node_executable_path.clone(),
|
||||
);
|
||||
|
||||
// Let caller finish the progress bar for this step
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_zip(&self, archive_path: &Path, target_dir: &Path) -> Result<()> {
|
||||
let file = std::fs::File::open(archive_path).context("Failed to open zip archive")?;
|
||||
async fn extract_zip(
|
||||
&self,
|
||||
archive_path: &Path,
|
||||
target_dir: &Path,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()> {
|
||||
let archive_path = archive_path.to_path_buf();
|
||||
let target_dir = target_dir.to_path_buf();
|
||||
let progress = progress.cloned();
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
let file = std::fs::File::open(&archive_path).context("Failed to open zip archive")?;
|
||||
let mut archive = zip::ZipArchive::new(file).context("Failed to read zip archive")?;
|
||||
|
||||
let mut archive = zip::ZipArchive::new(file).context("Failed to read zip archive")?;
|
||||
if let Some(pb) = &progress {
|
||||
pb.set_length(archive.len() as u64);
|
||||
pb.set_position(0);
|
||||
}
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).context("Failed to read zip entry")?;
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).context("Failed to read zip entry")?;
|
||||
|
||||
let outpath = match file.enclosed_name() {
|
||||
Some(path) => {
|
||||
// Remove the top-level directory from the path
|
||||
let components: Vec<_> = path.components().collect();
|
||||
if components.len() > 1 {
|
||||
target_dir.join(components[1..].iter().collect::<PathBuf>())
|
||||
} else {
|
||||
let outpath = match file.enclosed_name() {
|
||||
Some(path) => {
|
||||
let components: Vec<_> = path.components().collect();
|
||||
if components.len() > 1 {
|
||||
target_dir.join(components[1..].iter().collect::<PathBuf>())
|
||||
} else {
|
||||
if let Some(pb) = &progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(pb) = &progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
None => continue,
|
||||
};
|
||||
};
|
||||
|
||||
if file.is_dir() {
|
||||
fs::create_dir_all(&outpath)
|
||||
.await
|
||||
.context("Failed to create directory")?;
|
||||
} else {
|
||||
if let Some(p) = outpath.parent() {
|
||||
fs::create_dir_all(p)
|
||||
.await
|
||||
.context("Failed to create parent directory")?;
|
||||
if file.is_dir() {
|
||||
std::fs::create_dir_all(&outpath).context("Failed to create directory")?;
|
||||
} else {
|
||||
if let Some(p) = outpath.parent() {
|
||||
std::fs::create_dir_all(p).context("Failed to create parent directory")?;
|
||||
}
|
||||
|
||||
let mut outfile =
|
||||
std::fs::File::create(&outpath).context("Failed to create output file")?;
|
||||
|
||||
std::io::copy(&mut file, &mut outfile)
|
||||
.context("Failed to extract zip entry")?;
|
||||
}
|
||||
|
||||
let mut outfile = fs::File::create(&outpath)
|
||||
.await
|
||||
.context("Failed to create output file")?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
std::io::copy(&mut file, &mut buffer).context("Failed to read zip entry")?;
|
||||
|
||||
outfile
|
||||
.write_all(&buffer)
|
||||
.await
|
||||
.context("Failed to write output file")?;
|
||||
if let Some(pb) = &progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_tar_gz(&self, archive_path: &Path, target_dir: &Path) -> Result<()> {
|
||||
let output = tokio::process::Command::new("tar")
|
||||
.args([
|
||||
"-xzf",
|
||||
archive_path.to_str().unwrap(),
|
||||
"-C",
|
||||
target_dir.to_str().unwrap(),
|
||||
"--strip-components=1",
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to execute tar command")?;
|
||||
async fn extract_tar_gz(
|
||||
&self,
|
||||
archive_path: &Path,
|
||||
target_dir: &Path,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()> {
|
||||
let archive_path = archive_path.to_path_buf();
|
||||
let target_dir = target_dir.to_path_buf();
|
||||
let progress = progress.cloned();
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"tar extraction failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
use flate2::read::GzDecoder;
|
||||
use tar::Archive;
|
||||
|
||||
// First pass: count entries
|
||||
let file_for_count =
|
||||
std::fs::File::open(&archive_path).context("Failed to open tar.gz for counting")?;
|
||||
let decoder_for_count = GzDecoder::new(file_for_count);
|
||||
let mut archive_for_count = Archive::new(decoder_for_count);
|
||||
let mut total_entries: u64 = 0;
|
||||
for _ in archive_for_count
|
||||
.entries()
|
||||
.context("Failed to iterate tar entries")?
|
||||
{
|
||||
total_entries += 1;
|
||||
}
|
||||
|
||||
if let Some(pb) = &progress {
|
||||
pb.set_length(total_entries);
|
||||
pb.set_position(0);
|
||||
}
|
||||
|
||||
// Second pass: extract
|
||||
let file = std::fs::File::open(&archive_path).context("Failed to open tar.gz")?;
|
||||
let decoder = GzDecoder::new(file);
|
||||
let mut archive = Archive::new(decoder);
|
||||
|
||||
for entry in archive.entries().context("Failed to iterate tar entries")? {
|
||||
let mut entry = entry.context("Failed to read tar entry")?;
|
||||
let path = entry.path().context("Failed to get tar entry path")?;
|
||||
|
||||
// Strip the first component from the path
|
||||
let mut components = path.components();
|
||||
// discard first component
|
||||
components.next();
|
||||
let stripped: PathBuf = components.collect();
|
||||
if stripped.as_os_str().is_empty() {
|
||||
if let Some(pb) = &progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let outpath = target_dir.join(stripped);
|
||||
if let Some(parent) = outpath.parent() {
|
||||
std::fs::create_dir_all(parent).ok();
|
||||
}
|
||||
entry
|
||||
.unpack(&outpath)
|
||||
.context("Failed to unpack tar entry")?;
|
||||
|
||||
if let Some(pb) = &progress {
|
||||
pb.inc(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_full_version_spec(spec: &str) -> Option<String> {
|
||||
let cleaned = spec.trim().trim_start_matches('v');
|
||||
let parts: Vec<&str> = cleaned.split('.').collect();
|
||||
if parts.len() == 3 && parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit())) {
|
||||
Some(cleaned.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use log::{debug, info};
|
||||
use std::process::Command;
|
||||
|
||||
/// Manages Rust toolchain requirements and installation
|
||||
@@ -13,7 +14,7 @@ impl RustToolchain {
|
||||
match rustc_output {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version = String::from_utf8_lossy(&output.stdout);
|
||||
println!("Found Rust compiler: {}", version.trim());
|
||||
debug!("Found Rust compiler: {}", version.trim());
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -28,7 +29,7 @@ impl RustToolchain {
|
||||
match cargo_output {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version = String::from_utf8_lossy(&output.stdout);
|
||||
println!("Found Cargo: {}", version.trim());
|
||||
debug!("Found Cargo: {}", version.trim());
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -43,7 +44,7 @@ impl RustToolchain {
|
||||
match rustup_output {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version = String::from_utf8_lossy(&output.stdout);
|
||||
println!("Found rustup: {}", version.trim());
|
||||
debug!("Found rustup: {}", version.trim());
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -66,7 +67,7 @@ impl RustToolchain {
|
||||
let installed_targets = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if !installed_targets.contains(target) {
|
||||
println!("Installing Rust target: {target}");
|
||||
info!("Installing Rust target: {target}");
|
||||
let install_output = Command::new("rustup")
|
||||
.args(["target", "add", target])
|
||||
.output()
|
||||
@@ -76,9 +77,9 @@ impl RustToolchain {
|
||||
let stderr = String::from_utf8_lossy(&install_output.stderr);
|
||||
anyhow::bail!("Failed to install target {}:\n{}", target, stderr);
|
||||
}
|
||||
println!("Successfully installed target: {target}");
|
||||
info!("Successfully installed target: {target}");
|
||||
} else {
|
||||
println!("Target {target} is already installed");
|
||||
debug!("Target {target} is already installed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user