diff --git a/Cargo.lock b/Cargo.lock index ce7dbe51..74995894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -67,6 +73,19 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi-to-tui" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c" +dependencies = [ + "nom", + "ratatui", + "simdutf8", + "smallvec", + "thiserror 1.0.69", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -128,9 +147,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arg_parser" @@ -374,6 +393,21 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.29" @@ -395,6 +429,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -446,7 +486,7 @@ dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width 0.1.14", "vec_map", @@ -458,6 +498,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if 1.0.1", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "console" version = "0.15.11" @@ -467,7 +521,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width 0.2.0", "windows-sys 0.59.0", ] @@ -602,6 +656,41 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.4.0" @@ -683,6 +772,12 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dryoc" version = "0.6.2" @@ -784,6 +879,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "flate2" version = "1.1.2" @@ -974,9 +1080,17 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1211,6 +1325,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1267,10 +1387,19 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width 0.2.0", "web-time", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "inout" version = "0.1.4" @@ -1280,6 +1409,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instability" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "io-uring" version = "0.7.8" @@ -1379,6 +1521,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1402,6 +1553,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1422,6 +1579,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.1", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1500,6 +1679,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbr" version = "1.1.1" @@ -1627,6 +1812,27 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "portable-pty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -1676,7 +1882,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -1716,7 +1922,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "once_cell", "socket2", @@ -1804,6 +2010,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384c2842d4e069d5ccacf5fe1dca4ef8d07a5444329715f0fc3c61813502d4d1" +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "termion", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1863,13 +2090,19 @@ dependencies = [ name = "redox_cookbook" version = "0.1.0" dependencies = [ + "ansi-to-tui", + "anyhow", "blake3 1.5.3", + "filedescriptor", "ignore", + "libc", "object", "pbr", "pkgar 0.1.19", "pkgar-core 0.1.19", "pkgar-keys 0.1.19", + "portable-pty", + "ratatui", "redox-pkg", "redoxer", "regex", @@ -2290,6 +2523,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serial2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" +dependencies = [ + "cfg-if 1.0.1", + "libc", + "winapi", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2301,12 +2545,34 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.10" @@ -2356,6 +2622,34 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2723,6 +3017,23 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "unicode-width" version = "0.1.14" @@ -2731,9 +3042,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "untrusted" @@ -3256,6 +3567,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index e163cf5e..e21950d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "redox_cookbook" version = "0.1.0" authors = ["Jeremy Soller "] edition = "2024" -default-run = "cook" +default-run = "repo" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -21,13 +21,16 @@ path = "src/lib.rs" doctest = false [dependencies] +anyhow = "1" blake3 = "=1.5.3" # 1.5.4 is incompatible with blake3 0.3 dependency from pkgar +libc = "0.2" ignore = "0.4" object = { version = "0.36", features = ["build_core"] } pbr = "1.0.2" pkgar = { path = "pkgar/pkgar" } pkgar-core = { path = "pkgar/pkgar-core" } pkgar-keys = { path = "pkgar/pkgar-keys" } +portable-pty = "0.9.0" redox-pkg = "0.2.8" redoxer = "0.2" regex = "1.11" @@ -35,6 +38,13 @@ serde = { version = "=1.0.197", features = ["derive"] } termion = "4" toml = "0.8" walkdir = "2.3.1" +filedescriptor = "0.8.3" +ansi-to-tui = "7.0.0" + +[dependencies.ratatui] +version = "0.29.0" +default-features = false +features = ["termion"] [dev-dependencies] tempfile = "3" diff --git a/README.md b/README.md index 9da5af7b..04e10b34 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,16 @@ Cookbook has special config to avoid repetitive args, place this file into `cook ```toml # Configuration file # This is a configuration file to avoid repetitively spelling command args. -# At the moment only mirrors here implemented but in future it will be expanded when scripts are rusted +# At the moment this configures mirror and cook configuration + +# These options has defaults set below +# These options has higher priority than env +#[cook] +#jobs = +#nonstop = false +#offline = false +#tui = true +#verbose = true [mirrors] # see list of GNU FTP mirrors at https://www.gnu.org/prep/ftp.en.html diff --git a/clean.sh b/clean.sh index 6f7d0b0f..efde8b91 100755 --- a/clean.sh +++ b/clean.sh @@ -1,19 +1,13 @@ #!/usr/bin/env bash set -e -source config.sh +source `dirname "$0"`/config.sh if [ $# = 0 ] then - recipes="$(list_recipes --short)" + recipes="--all" else recipes="$@" fi -for recipe_name in $recipes -do - recipe_path=`find_recipe $recipe_name` - - echo -e "\033[01;38;5;215mcook - clean $recipe_name\033[0m" - rm -rf "${ROOT}/$recipe_path/target/${TARGET}" -done +repo clean $recipes diff --git a/config.sh b/config.sh index c3eccb17..8e980198 100755 --- a/config.sh +++ b/config.sh @@ -17,8 +17,11 @@ if [ x"${HOST}" == x"riscv64gc-unknown-redox" ] ; then HOST="riscv64-unknown-redox" fi +# Cookbook requires correct CWD to work +cd `dirname "$0"` + # Automatic variables -ROOT="$(cd `dirname "$0"` && pwd)" +ROOT=`pwd` export AR="${HOST}-gcc-ar" export AS="${HOST}-as" @@ -60,6 +63,9 @@ function pkgar { function cook { "$ROOT/target/release/cook" "$@" } +function repo { + "$ROOT/target/release/repo" "$@" +} function repo_builder { "$ROOT/target/release/repo_builder" "$@" } diff --git a/fetch.sh b/fetch.sh index 73da2648..cd9acc9c 100755 --- a/fetch.sh +++ b/fetch.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -e -source config.sh +source `dirname "$0"`/config.sh -cook --fetch-only ${@:1} +# Intentionally empty to allow fetch and cook running in parallel diff --git a/repo.sh b/repo.sh index ddc0e779..0a95ac89 100755 --- a/repo.sh +++ b/repo.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash set -e -shopt -s nullglob -source config.sh +source `dirname "$0"`/config.sh APPSTREAM="0" COOK_OPT="" @@ -20,15 +19,12 @@ do COOK_OPT+=" --nonstop" elif [ "$arg" == "--offline" ] then - COOK_OPT+=" --offline" + export COOKBOOK_OFFLINE=true else recipes+=" $arg" fi done -cook $COOK_OPT $recipes +repo cook $COOK_OPT $recipes -repo="$ROOT/repo/$TARGET" -mkdir -p "$repo" - -repo_builder "$repo" $recipes +repo_builder "$ROOT/repo/$TARGET" $recipes diff --git a/src/bin/cook.rs b/src/bin/cook.rs index 38d126c9..e70bf4bb 100644 --- a/src/bin/cook.rs +++ b/src/bin/cook.rs @@ -1,36 +1,17 @@ -use std::collections::BTreeSet; use std::path::Path; use std::{env, process}; use cookbook::WALK_DEPTH; use cookbook::cook::fetch::{fetch, fetch_offline}; use cookbook::cook::fs::create_target_dir; -use cookbook::cook::package::{package, package_toml}; -use cookbook::recipe::{BuildKind, CookRecipe, Recipe}; +use cookbook::cook::package::package; +use cookbook::recipe::{CookRecipe, Recipe}; use pkg::PackageName; use cookbook::config::init_config; use cookbook::cook::cook_build::build; use termion::{color, style}; -fn cook_meta( - recipe_dir: &Path, - name: &PackageName, - recipe: &Recipe, - fetch_only: bool, -) -> Result<(), String> { - if fetch_only { - return Ok(()); - } - - let target_dir = create_target_dir(recipe_dir)?; - let empty_deps = BTreeSet::new(); - let _package_file = package_toml(&target_dir, name, recipe, &empty_deps) - .map_err(|err| format!("failed to package: {}", err))?; - - Ok(()) -} - fn cook( recipe_dir: &Path, name: &PackageName, @@ -39,12 +20,9 @@ fn cook( fetch_only: bool, is_offline: bool, ) -> Result<(), String> { - if recipe.build.kind == BuildKind::None { - return cook_meta(recipe_dir, name, recipe, fetch_only); - } let source_dir = match is_offline { - true => fetch_offline(recipe_dir, &recipe.source), - false => fetch(recipe_dir, &recipe.source), + true => fetch_offline(recipe_dir, recipe, &None), + false => fetch(recipe_dir, recipe, &None), } .map_err(|err| format!("failed to fetch: {}", err))?; @@ -62,10 +40,11 @@ fn cook( recipe, is_offline, !is_deps, + &None, ) .map_err(|err| format!("failed to build: {}", err))?; - let _package_file = package(&stage_dir, &target_dir, name, recipe, &auto_deps) + package(&stage_dir, &target_dir, name, recipe, &auto_deps, &None) .map_err(|err| format!("failed to package: {}", err))?; Ok(()) diff --git a/src/bin/repo.rs b/src/bin/repo.rs new file mode 100644 index 00000000..01fd77ac --- /dev/null +++ b/src/bin/repo.rs @@ -0,0 +1,1351 @@ +use ansi_to_tui::IntoText; +use anyhow::{Context, anyhow, bail}; +use cookbook::WALK_DEPTH; +use cookbook::config::{CookConfig, get_config, init_config}; +use cookbook::cook::cook_build::build; +use cookbook::cook::fetch::{fetch, fetch_offline}; +use cookbook::cook::fs::create_target_dir; +use cookbook::cook::package::package; +use cookbook::cook::pty::{PtyOut, UnixSlavePty, setup_pty}; +use cookbook::recipe::CookRecipe; +use pkg::PackageName; +use pkg::package::PackageError; +use ratatui::Terminal; +use ratatui::layout::{Constraint, Direction, Layout, Position, Rect}; +use ratatui::prelude::TermionBackend; +use ratatui::style::{Color, Style}; +use ratatui::text::{Line, Span, Text}; +use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}; +use redoxer::target; +use std::borrow::Cow; +use std::collections::{HashMap, VecDeque}; +use std::io::{Read, Write, stderr, stdin, stdout}; +use std::path::PathBuf; +use std::process::Command; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::{Arc, mpsc}; +use std::time::{Duration, Instant}; +use std::{cmp, env, fs}; +use std::{process, thread}; +use termion::event::{Event, Key, MouseEvent}; +use termion::input::TermRead; +use termion::raw::IntoRawMode; +use termion::screen::IntoAlternateScreen; +use termion::{color, style}; + +// A repo manager, to replace repo.sh + +const REPO_HELP_STR: &str = r#" + Usage: repo [flags] ... + + command list: + fetch download recipe sources + cook build recipe packages + unfetch delete recipe sources + clean delete recipe artifacts + push extract package into sysroot + find find path of recipe packages + tree show tree of recipe packages + + common flags: + --cookbook= the "recipes" folder, default to $PWD/recipes + --repo= the "repo" folder, default to $PWD/repo + --sysroot= the "root" folder used for "push" command + For Redox, defaults to "/", else default to $PWD/sysroot + --with-package-deps include package deps + --all apply to all recipes in + --category= apply to all recipes in / + + cook env and their defaults: + CI= set to any value to disable TUI + COOKBOOK_OFFLINE=false prevent internet access if possible + COOKBOOK_NONSTOP=false pkeep running even a recipe build failed + COOKBOOK_VERBOSE=true print success/error on each recipe + COOKBOOK_MAKE_JOBS= override build jobs count from nproc +"#; + +#[derive(Clone)] +struct CliConfig { + cookbook_dir: PathBuf, + repo_dir: PathBuf, + sysroot_dir: PathBuf, + category: Option, + with_package_deps: bool, + all: bool, + cook: CookConfig, +} + +#[derive(PartialEq)] +enum CliCommand { + Fetch, + Cook, + Unfetch, + Clean, + Push, + Tree, + Find, +} + +impl CliCommand { + pub fn is_informational(&self) -> bool { + *self == CliCommand::Tree || *self == CliCommand::Find + } + pub fn is_building(&self) -> bool { + *self == CliCommand::Fetch || *self == CliCommand::Cook + } +} + +impl FromStr for CliCommand { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "fetch" => Ok(CliCommand::Fetch), + "cook" => Ok(CliCommand::Cook), + "unfetch" => Ok(CliCommand::Unfetch), + "clean" => Ok(CliCommand::Clean), + "push" => Ok(CliCommand::Push), + "tree" => Ok(CliCommand::Tree), + "find" => Ok(CliCommand::Find), + _ => Err(anyhow!("Unknown command '{}'\n{}\n", s, REPO_HELP_STR)), + } + } +} + +impl ToString for CliCommand { + fn to_string(&self) -> String { + match self { + CliCommand::Fetch => "fetch".to_string(), + CliCommand::Cook => "cook".to_string(), + CliCommand::Unfetch => "unfetch".to_string(), + CliCommand::Clean => "clean".to_string(), + CliCommand::Push => "push".to_string(), + CliCommand::Tree => "tree".to_string(), + CliCommand::Find => "find".to_string(), + } + } +} + +impl CliConfig { + fn new() -> Result { + let current_dir = env::current_dir()?; + Ok(CliConfig { + //FIXME: This config is unused as redox-pkg harcoded this to $PWD/recipes + cookbook_dir: current_dir.join("recipes"), + repo_dir: current_dir.join("repo"), + category: None, + sysroot_dir: if cfg!(target_os = "redox") { + PathBuf::from("/") + } else { + current_dir.join("sysroot") + }, + with_package_deps: false, + cook: get_config().cook.clone(), + all: false, + }) + } +} + +fn main() { + init_config(); + if let Err(e) = main_inner() { + eprintln!("{:?}", e); + process::exit(1); + }; +} + +fn main_inner() -> anyhow::Result<()> { + let args: Vec = env::args().skip(1).collect(); + + if args.is_empty() || args.contains(&"--help".to_string()) || args.contains(&"-h".to_string()) { + println!("{}", REPO_HELP_STR); + process::exit(1); + } + + let (config, command, recipe_names) = parse_args(args)?; + if command == CliCommand::Cook && config.cook.tui { + if let Some((name, e)) = run_tui_cook(config, recipe_names)? { + let _ = stderr().write(e.as_bytes()); + let _ = stderr().write(b"\n\n"); + eprintln!( + "{}{}cook - failed at {}{}{}", + style::Bold, + color::Fg(color::AnsiValue(196)), + name.as_str(), + color::Fg(color::Reset), + style::Reset, + ); + return Err(anyhow!("Execution has failed")); + } else { + eprintln!( + "{}{}cook - successful{}{}", + style::Bold, + color::Fg(color::AnsiValue(46)), + color::Fg(color::Reset), + style::Reset, + ); + } + return Ok(()); + } + + let verbose = config.cook.verbose; + for recipe in &recipe_names { + match repo_inner(&config, &command, recipe) { + Ok(_) => { + if verbose { + eprintln!( + "{}{}{} {} - successful{}{}", + style::Bold, + color::Fg(color::AnsiValue(46)), + command.to_string(), + recipe.name.as_str(), + color::Fg(color::Reset), + style::Reset, + ); + } + } + Err(e) => { + if config.cook.nonstop && verbose { + eprintln!("{:?}", e); + } + eprintln!( + "{}{}{} {} - failed {}{}", + style::Bold, + color::Fg(color::AnsiValue(196)), + command.to_string(), + recipe.name.as_str(), + color::Fg(color::Reset), + style::Reset, + ); + if !config.cook.nonstop { + return Err(e); + } + } + } + } + + if verbose { + println!( + "\nCommand '{}' completed for {} recipes.", + command.to_string(), + recipe_names.len() + ); + } + Ok(()) +} + +fn repo_inner( + config: &CliConfig, + command: &CliCommand, + recipe: &CookRecipe, +) -> Result<(), anyhow::Error> { + Ok(match *command { + CliCommand::Fetch => { + handle_fetch(recipe, config, &None)?; + } + CliCommand::Cook => { + let source_dir = handle_fetch(recipe, config, &None)?; + handle_cook(recipe, config, source_dir, recipe.is_deps, &None)? + } + CliCommand::Unfetch => handle_clean(recipe, config, true, true)?, + CliCommand::Clean => handle_clean(recipe, config, false, true)?, + CliCommand::Push => handle_push(recipe, config)?, + CliCommand::Tree => todo!("tree command is WIP"), + CliCommand::Find => println!("{}", recipe.dir.display()), + }) +} + +fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec)> { + let mut config = CliConfig::new()?; + let mut command: Option = None; + let mut recipe_names: Vec = Vec::new(); + for arg in args { + if arg.starts_with("--") { + if let Some((key, value)) = arg.split_once('=') { + match key { + "--cookbook" => config.cookbook_dir = PathBuf::from(value), + "--repo" => config.repo_dir = PathBuf::from(value), + "--sysroot" => config.sysroot_dir = PathBuf::from(value), + "--category" => config.category = Some(PathBuf::from(value)), + _ => { + eprintln!("Error: Unknown flag with value: {}", arg); + process::exit(1); + } + } + } else if arg.starts_with("--category-") { + // to workaround make command limit we provide this option + config.category = Some(PathBuf::from(arg[("--category-").len()..].to_owned())); + } else { + match arg.as_str() { + "--with-package-deps" => config.with_package_deps = true, + "--all" => config.all = true, + _ => { + eprintln!("Error: Unknown flag: {}", arg); + process::exit(1); + } + } + } + } else if arg.starts_with('-') { + match arg.as_str() { + _ => { + eprintln!("Error: Unknown flag: {}", arg); + process::exit(1); + } + } + } else if command.is_none() { + // The first non-flag argument is the command + command = Some(arg); + } else { + // Subsequent non-flag arguments are recipe names + recipe_names.push(arg.try_into().context("Invalid package name")?); + } + } + + if let Some(c) = config.category { + // need to prefix by cookbook dir + config.category = Some(PathBuf::from("recipes").join(c)); + } + + let command = command.ok_or(anyhow!("Error: No command specified."))?; + let command: CliCommand = str::parse(&command)?; + let recipes = if config.all || config.category.is_some() { + if !recipe_names.is_empty() { + bail!("Do not specify recipe names when using the --all or --category flag."); + } + if config.all && config.category.is_some() { + bail!("Do not specify both --all and --category flag."); + } + if config.all && command != CliCommand::Clean && command != CliCommand::Unfetch { + // because read_recipe is false by logic below + // some recipes on wip folders are invalid anyway + bail!( + "Refusing to run an unrealistic command to {} all recipes", + command.to_string() + ); + } + match &config.category { + None => pkg::recipes::list(""), + Some(prefix) => pkg::recipes::list("") + .into_iter() + .filter(|p| p.starts_with(prefix)) + .collect(), + } + .iter() + .map(|f| CookRecipe::from_path(f, !config.all)) + .collect::, PackageError>>()? + } else { + if recipe_names.is_empty() { + bail!("Error: No recipe names provided and --all flag was not used."); + } + if config.with_package_deps { + recipe_names = CookRecipe::get_package_deps_recursive(&recipe_names, WALK_DEPTH) + .context("failed get package deps")?; + } + + if command.is_building() { + CookRecipe::get_build_deps_recursive(&recipe_names, !config.with_package_deps)? + } else { + recipe_names + .iter() + .map(|f| CookRecipe::from_name(f.as_str()).unwrap()) + .collect() + } + }; + + if command.is_informational() { + // avoid extra data that clobber stdout + config.cook.verbose = false; + } + + Ok((config, command, recipes)) +} + +fn handle_fetch( + recipe: &CookRecipe, + config: &CliConfig, + logger: &PtyOut, +) -> anyhow::Result { + let recipe_dir = &recipe.dir; + let source_dir = match config.cook.offline { + true => fetch_offline(recipe_dir, &recipe.recipe, logger), + false => fetch(recipe_dir, &recipe.recipe, logger), + } + .map_err(|e| anyhow!("failed to fetch: {:?}", e))?; + + Ok(source_dir) +} + +fn handle_cook( + recipe: &CookRecipe, + config: &CliConfig, + source_dir: PathBuf, + is_deps: bool, + logger: &PtyOut, +) -> anyhow::Result<()> { + let recipe_dir = &recipe.dir; + let target_dir = create_target_dir(recipe_dir).map_err(|e| anyhow!(e))?; + let (stage_dir, auto_deps) = build( + recipe_dir, + &source_dir, + &target_dir, + &recipe.name, + &recipe.recipe, + config.cook.offline, + !is_deps, + logger, + ) + .map_err(|err| anyhow!("failed to build: {:?}", err))?; + + package( + &stage_dir, + &target_dir, + &recipe.name, + &recipe.recipe, + &auto_deps, + logger, + ) + .map_err(|err| anyhow!("failed to package: {:?}", err))?; + + Ok(()) +} + +fn handle_clean( + recipe: &CookRecipe, + _config: &CliConfig, + source: bool, + target: bool, +) -> anyhow::Result<()> { + let dir = recipe.dir.join("target"); + if dir.exists() && target { + fs::remove_dir_all(&dir).context(format!("failed to delete {}", dir.display()))?; + } + let dir = recipe.dir.join("source"); + if dir.exists() && source { + fs::remove_dir_all(&dir).context(format!("failed to delete {}", dir.display()))?; + } + Ok(()) +} + +fn handle_push(recipe: &CookRecipe, config: &CliConfig) -> anyhow::Result<()> { + let public_path = "build/id_ed25519.pub.toml"; + let archive_path = config + .repo_dir + .join(target()) + .join(format!("{}.pkgar", recipe.name)); + pkgar::extract( + public_path, + archive_path.as_path(), + config.sysroot_dir.to_str().unwrap(), + ) + .context(format!( + "failed to install '{}' in '{}'", + archive_path.display(), + config.sysroot_dir.display(), + )) +} + +// +// ------------- TUI SPECIFIC CODE ------------------- +// + +#[derive(Debug, Clone, PartialEq)] +enum RecipeStatus { + Pending, + Fetching, + Fetched, + Cooking, + Done, + Failed(String), +} + +#[derive(Debug, Clone)] +enum StatusUpdate { + StartFetch(PackageName), + Fetched(CookRecipe), + FailFetch(CookRecipe, String), + StartCook(PackageName), + Cooked(CookRecipe), + FailCook(CookRecipe, String), + PushLog(PackageName, Vec), + FetchThreadFinished, + CookThreadFinished, +} + +#[derive(PartialEq)] +enum JobType { + Fetch, + Cook, +} + +impl ToString for JobType { + fn to_string(&self) -> String { + match self { + JobType::Fetch => "Fetch", + JobType::Cook => "Cook", + } + .to_string() + } +} + +struct TuiApp { + recipes: Vec<(CookRecipe, RecipeStatus)>, + fetch_queue: VecDeque, + cook_queue: VecDeque, + done: Vec, + active_fetch: Option, + active_cook: Option, + logs: HashMap>, + log_byte_buffer: HashMap>, + log_scroll: usize, + log_view_job: JobType, + auto_scroll: bool, + fetch_scroll: usize, + cook_scroll: usize, + cook_auto_scroll: bool, + cook_list_state: ListState, + fetch_complete: bool, + cook_complete: bool, + fetch_panel_rect: Option, + cook_panel_rect: Option, + log_panel_rect: Option, + prompt: Option, + dump_logs_on_exit: Option<(PackageName, String)>, +} + +impl TuiApp { + fn new(recipes: Vec) -> Self { + Self { + recipes: recipes + .iter() + .cloned() + .map(|r| (r, RecipeStatus::Pending)) + .collect(), + fetch_queue: recipes.iter().cloned().map(|r| r.clone()).collect(), + cook_queue: VecDeque::new(), + done: Vec::new(), + active_fetch: None, + active_cook: None, + logs: HashMap::new(), + log_byte_buffer: HashMap::new(), + log_scroll: 0, + auto_scroll: true, + log_view_job: JobType::Fetch, + fetch_scroll: 0, + cook_scroll: 0, + cook_auto_scroll: true, + cook_list_state: ListState::default(), + fetch_complete: false, + cook_complete: false, + fetch_panel_rect: None, + cook_panel_rect: None, + log_panel_rect: None, + prompt: None, + dump_logs_on_exit: None, + } + } + + // Update the state based on a message from a worker thread + fn update_status(&mut self, update: StatusUpdate) { + let (name, new_status) = match update { + StatusUpdate::StartFetch(name) => { + self.active_fetch = Some(name.clone()); + self.logs.insert(name.clone(), Vec::new()); + self.log_byte_buffer.insert(name.clone(), Vec::new()); + self.log_scroll = 0; + self.auto_scroll = true; + (name.clone(), RecipeStatus::Fetching) + } + StatusUpdate::Fetched(recipe) => (recipe.name.clone(), RecipeStatus::Fetched), + StatusUpdate::FailFetch(recipe, err) => { + self.prompt = Some(FailurePrompt::new(recipe.clone(), err.clone())); + (recipe.name.clone(), RecipeStatus::Failed(err)) + } + StatusUpdate::StartCook(name) => { + self.active_cook = Some(name.clone()); + self.logs.insert(name.clone(), Vec::new()); + self.log_byte_buffer.insert(name.clone(), Vec::new()); + (name.clone(), RecipeStatus::Cooking) + } + StatusUpdate::PushLog(name, chunk) => { + let buffer = self.log_byte_buffer.entry(name.clone()).or_default(); + buffer.extend_from_slice(&chunk); + let log_list = self.logs.entry(name.clone()).or_default(); + while let Some(newline_pos) = buffer.iter().position(|&b| b == b'\n') { + let line_bytes = buffer.drain(..=newline_pos).collect::>(); + let line_str = String::from_utf8_lossy(&line_bytes).into_owned(); + let line_str_pos = line_str.trim_end(); + let line_str = line_str_pos.rsplit('\r').next().unwrap_or(&line_str_pos); + log_list.push(line_str.to_owned()); + } + return; + } + StatusUpdate::Cooked(recipe) => { + if self.active_cook.as_ref() == Some(&recipe.name) { + self.active_cook = None; + } + self.auto_scroll = true; + (recipe.name.clone(), RecipeStatus::Done) + } + StatusUpdate::FailCook(recipe, err) => { + self.prompt = Some(FailurePrompt::new(recipe.clone(), err.clone())); + (recipe.name.clone(), RecipeStatus::Failed(err)) + } + StatusUpdate::FetchThreadFinished => { + self.fetch_complete = true; + self.log_view_job = JobType::Cook; + return; + } + StatusUpdate::CookThreadFinished => { + self.cook_complete = true; + return; + } + }; + + if let Some((_, status)) = self.recipes.iter_mut().find(|(r, _)| r.name == name) { + *status = new_status; + } + + // Re-compute the queues for display + self.fetch_queue = self + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Pending) + .map(|(r, _)| r.clone()) + .collect(); + self.cook_queue = self + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Fetched) + .map(|(r, _)| r.clone()) + .collect(); + self.done = self + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Done) + .map(|(r, _)| r.name.clone()) + .collect(); + } +} + +fn run_tui_cook( + config: CliConfig, + recipes: Vec, +) -> anyhow::Result> { + let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>(); + let (status_tx, status_rx) = mpsc::channel::(); + + let running = Arc::new(AtomicBool::new(true)); + let prompting = Arc::new(AtomicU32::new(0)); + const TICK_RATE: Duration = Duration::from_millis(100); + + // ---- Cooker Thread ---- + let cooker_config = config.clone(); + let cooker_status_tx = status_tx.clone(); + let cooker_prompting = prompting.clone(); + let cooker_handle = thread::spawn(move || { + 'done: for (recipe, source_dir) in work_rx { + let name = recipe.name.clone(); + let is_deps = recipe.is_deps; + cooker_status_tx + .send(StatusUpdate::StartCook(name.clone())) + .unwrap(); + let (mut stdout_writer, mut stderr_writer) = setup_logger(&cooker_status_tx, &name); + let logger = Some((&mut stdout_writer, &mut stderr_writer)); + 'again: loop { + match handle_cook( + &recipe, + &cooker_config, + source_dir.clone(), + is_deps, + &logger, + ) { + Ok(()) => { + cooker_status_tx + .send(StatusUpdate::Cooked(recipe)) + .unwrap_or_default(); + break; + } + Err(e) => { + cooker_status_tx + .send(StatusUpdate::FailCook(recipe.clone(), e.to_string())) + .unwrap_or_default(); + if !cooker_config.cook.nonstop { + while cooker_prompting.load(Ordering::SeqCst) != 0 { + thread::sleep(Duration::from_millis(101)); // wait other prompt + } + cooker_prompting.swap(1, Ordering::SeqCst); + 'wait: loop { + match cooker_prompting.load(Ordering::SeqCst) { + 0 => break 'again, + 1 => thread::sleep(Duration::from_millis(101)), + 2 => { + cooker_prompting.swap(0, Ordering::SeqCst); + break 'wait; + } // retry + 3 => { + cooker_prompting.swap(0, Ordering::SeqCst); + break 'again; + } // skip + 4 => { + cooker_prompting.swap(0, Ordering::SeqCst); + break 'done; + } // done + _ => unreachable!(), + } + } + } + } + } + } + } + cooker_status_tx + .send(StatusUpdate::CookThreadFinished) + .unwrap_or_default(); + }); + + let mstdin = stdin(); + let mstdout = stdout() + .into_raw_mode() + .unwrap() + .into_alternate_screen() + .unwrap(); + + // ----- Input Thread ----- + let (input_tx, input_rx) = mpsc::channel::(); + let _input_handle = thread::spawn(move || { + for evt in mstdin.events() { + if let Ok(evt) = evt { + if input_tx.send(evt).is_err() { + return; + } + } + } + }); + + // ---- Fetcher Thread ---- + let fetcher_recipes = recipes.clone(); + let fetcher_status_tx = status_tx.clone(); + let fetcher_config = config.clone(); + let fetcher_prompting = prompting.clone(); + let fetcher_handle = thread::spawn(move || { + 'done: for recipe in fetcher_recipes { + let name = recipe.name.clone(); + fetcher_status_tx + .send(StatusUpdate::StartFetch(name.clone())) + .unwrap(); + + let (mut stdout_writer, mut stderr_writer) = setup_logger(&fetcher_status_tx, &name); + let logger = Some((&mut stdout_writer, &mut stderr_writer)); + + 'again: loop { + match handle_fetch(&recipe, &fetcher_config, &logger) { + Ok(source_dir) => { + fetcher_status_tx + .send(StatusUpdate::Fetched(recipe.clone())) + .unwrap(); + if work_tx.send((recipe.clone(), source_dir)).is_err() { + // Cooker thread died + break 'done; + } + break; + } + Err(e) => { + fetcher_status_tx + .send(StatusUpdate::FailFetch(recipe.clone(), e.to_string())) + .unwrap_or_default(); + if !fetcher_config.cook.nonstop { + while fetcher_prompting.load(Ordering::SeqCst) != 0 { + thread::sleep(Duration::from_millis(101)); // wait other prompt + } + fetcher_prompting.swap(1, Ordering::SeqCst); + 'wait: loop { + match fetcher_prompting.load(Ordering::SeqCst) { + 0 => break 'again, + 1 => thread::sleep(Duration::from_millis(101)), + 2 => { + fetcher_prompting.swap(0, Ordering::SeqCst); + break 'wait; + } // retry + 3 => { + fetcher_prompting.swap(0, Ordering::SeqCst); + break 'again; + } // skip + 4 => { + fetcher_prompting.swap(0, Ordering::SeqCst); + break 'done; + } // done + _ => unreachable!(), + } + } + } + } + } + } + } + status_tx + .send(StatusUpdate::FetchThreadFinished) + .unwrap_or_default(); + }); + + let mut terminal = Terminal::new(TermionBackend::new(stdout()))?; + terminal.clear()?; + + let mut app = TuiApp::new(recipes); + + let spinner = ['-', '\\', '|', '/']; + let mut spinner_i = 0; + + while running.load(Ordering::SeqCst) { + let frame_start = Instant::now(); + terminal.draw(|f| { + spinner_i = (spinner_i + 1) % spinner.len(); + let spin = spinner[spinner_i]; + + let mut constraints = Vec::new(); + if !app.fetch_complete { + constraints.push(Constraint::Length(22)); + } + constraints.push(Constraint::Length(22)); + constraints.push(Constraint::Min(20)); + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints) + .split(f.area()); + let panel_height = chunks[0].height.saturating_sub(2) as usize; + + // Left Pane + let fetch_items: Vec = app + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Pending || *s == RecipeStatus::Fetching) + .map(|(r, s)| { + let style = if *s == RecipeStatus::Fetching { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + let icon = match s { + RecipeStatus::Pending => ' ', + RecipeStatus::Fetching => spin, + _ => '?', + }; + + ListItem::new(format!("{icon} {}", r.name)).style(style) + }) + .collect(); + let fetch_list = List::new(fetch_items).block( + Block::default() + .title("Fetch Queue [1]") + .borders(Borders::ALL), + ); + f.render_widget(fetch_list, chunks[0]); + + // Right Pane + let cook_items: Vec = app + .recipes + .iter() + .filter(|(_, s)| { + *s == RecipeStatus::Fetched + || *s == RecipeStatus::Cooking + || *s == RecipeStatus::Done + || matches!(s, RecipeStatus::Failed(_)) + }) + .map(|(r, s)| { + let style = match s { + RecipeStatus::Fetched => Style::default().fg(Color::Cyan), + RecipeStatus::Cooking => Style::default().fg(Color::Yellow), + RecipeStatus::Done => Style::default().fg(Color::Green), + RecipeStatus::Failed(_) => Style::default().fg(Color::Red), + _ => Style::default(), + }; + let icon = match s { + RecipeStatus::Fetched => ' ', + RecipeStatus::Cooking => spin, + RecipeStatus::Done => ' ', + RecipeStatus::Failed(_) => 'X', + _ => '?', + }; + ListItem::new(format!("{icon} {}", r.name)).style(style) + }) + .collect(); + let total_items = cook_items.len(); + if app.cook_auto_scroll { + let cooking_index = app + .recipes + .iter() + .filter(|(_, s)| { + *s == RecipeStatus::Fetched + || *s == RecipeStatus::Cooking + || *s == RecipeStatus::Done + || matches!(s, RecipeStatus::Failed(_)) + }) + .position(|(_r, s)| *s == RecipeStatus::Cooking); + + if let Some(index) = cooking_index { + app.cook_list_state.select(Some(index)); + let index_u16 = index; + let center_offset = panel_height / 2; + let new_offset = index_u16.saturating_sub(center_offset) as usize; + + *app.cook_list_state.offset_mut() = new_offset; + } + } else { + app.cook_list_state.select(None); + if total_items > 0 { + let max_offset = total_items.saturating_sub(panel_height as usize); + if *app.cook_list_state.offset_mut() > max_offset { + *app.cook_list_state.offset_mut() = max_offset; + } + } else { + *app.cook_list_state.offset_mut() = 0; + } + } + let cook_items: Vec = cook_items[app.cook_scroll..].into(); + let cook_chunk = chunks[if app.fetch_complete { 0 } else { 1 }]; + let cook_list = List::new(cook_items).block( + Block::default() + .title("Cook Queue [2]") + .borders(Borders::ALL), + ); + f.render_stateful_widget(cook_list, cook_chunk, &mut app.cook_list_state); + + let (active_name, log_text, log_line) = get_active_log(&app); + let log_title = if let Some(active_name) = active_name { + format!( + " {} Log: {} ", + app.log_view_job.to_string(), + active_name.as_str() + ) + } else { + format!(" {} Log ", app.log_view_job.to_string()) + }; + + let mut enable_auto_scroll = false; + let mut intended_scroll_pos = 0usize; + + let mut log_lines: Vec = if let Some(log_text) = log_text + && !log_text.is_empty() + { + let total_log_lines = log_text.len() as usize; + + let start = if app.auto_scroll { + if total_log_lines > panel_height { + intended_scroll_pos = total_log_lines - panel_height; + total_log_lines - panel_height + } else { + 0 + } + } else { + if total_log_lines > panel_height { + if app.log_scroll >= total_log_lines - panel_height { + enable_auto_scroll = true; + intended_scroll_pos = total_log_lines - panel_height; + total_log_lines - panel_height + } else { + app.log_scroll + } + } else { + 0 + } + }; + + let end = cmp::min(panel_height + start, total_log_lines - 1); + + log_text[start..end] + .iter() + .map(|s| { + let text_with_colors = s + .into_text() + .unwrap_or_else(|_| Text::raw("--unrenderable line--")); + text_with_colors + .lines + .into_iter() + .next() + .unwrap_or_else(|| Line::raw("--unrenderable line--")) + }) + .collect() + } else { + vec![Line::from("No logs yet")] + }; + + if let Some(buffer) = log_line + && !buffer.is_empty() + { + let text_with_colors = handle_cr(&buffer) + .into_text() + .unwrap_or_else(|_| Text::raw("--unrenderable line--")); + + if let Some(line) = text_with_colors.lines.into_iter().next() { + log_lines.push(line); + } + } + + let instruct = format!( + " Keys: [c] Stop [PageUp/Down] Scroll{}{} ", + match app.auto_scroll { + true => "", + false => " [End] Follow log trails", + }, + match (&app.log_view_job, app.fetch_complete) { + (JobType::Fetch, _) => " [2] View Cook Log", + (JobType::Cook, false) => " [1] View Fetch Log", + (JobType::Cook, true) => "", + } + ); + + let log_paragraph = Paragraph::new(log_lines) + .block( + Block::default() + .title(log_title) + .title_bottom(instruct) + .borders(Borders::ALL), + ) + .wrap(Wrap { trim: false }); + + f.render_widget( + log_paragraph, + chunks[if app.fetch_complete { 1 } else { 2 }], + ); + if let Some(prompt) = &app.prompt { + draw_prompt(f, prompt); + } + if enable_auto_scroll { + app.auto_scroll = true; + } + if intended_scroll_pos > 0 { + app.log_scroll = intended_scroll_pos; + } + + while let Ok(event) = input_rx.try_recv() { + if let Some((app, res)) = handle_prompt_input(&event, &mut app) { + prompting.swap(res as u32, Ordering::SeqCst); + if res == PromptOption::Exit { + let (name, log, line) = get_active_log(&app); + if let Some(name) = name + && let Some(log) = log + { + let mut logs = log.join("\n"); + if let Some(line) = line { + logs.push_str("\n"); + logs.push_str(handle_cr(&line)); + } + app.dump_logs_on_exit = Some((name.to_owned(), logs)); + } + running.store(false, Ordering::SeqCst); + } + app.prompt = None; + } else { + handle_main_event(&mut app, &event); + } + } + })?; + + while let Ok(update) = status_rx.try_recv() { + app.update_status(update); + } + + if app.cook_complete { + running.swap(false, Ordering::SeqCst); + } + + if let Some(sleep_duration) = TICK_RATE.checked_sub(frame_start.elapsed()) { + thread::sleep(sleep_duration); + } + } + + drop(mstdout); + let _ = stdout().flush(); + + fetcher_handle.join().unwrap(); + cooker_handle.join().unwrap(); + + Ok(app.dump_logs_on_exit) +} + +fn handle_cr<'a>(buffer: &'a Cow<'_, str>) -> &'a str { + let st = buffer.trim_end(); + st.rsplit('\r').next().unwrap_or(&st) +} + +fn get_active_log( + app: &TuiApp, +) -> ( + Option, + Option<&Vec>, + Option>, +) { + let active_name = if app.log_view_job == JobType::Cook { + app.active_cook.clone() + } else { + app.active_fetch.clone() + }; + let log_text = if let Some(active_name) = &active_name { + app.logs.get(active_name) + } else { + None + }; + let log_line = if let Some(active_name) = &active_name + && let Some(b) = app.log_byte_buffer.get(active_name) + { + Some(String::from_utf8_lossy(b)) + } else { + None + }; + (active_name, log_text, log_line) +} + +fn handle_main_event(app: &mut TuiApp, event: &Event) { + match event { + Event::Key(key) => match key { + Key::Char('1') => { + app.log_view_job = JobType::Fetch; + } + Key::Char('2') => { + app.log_view_job = JobType::Cook; + } + Key::Char('c') => { + // as compilers still running, we use this way to stop it + let pid = std::process::id(); + Command::new("pkill") + .arg("-9") + .arg("-P") + .arg(pid.to_string()) + .spawn() + .expect("unable to spawn pkill"); + } + Key::Up => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(1); + } + Key::Down => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(1); + } + Key::PageUp => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(20); + } + Key::PageDown => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(20); + } + Key::End => { + app.auto_scroll = true; + } + Key::Home => { + app.auto_scroll = false; + app.log_scroll = 0; + } + _ => {} + }, + + //FIXME: This does nothing, it seems ratatui handles this itself magically + Event::Mouse(mouse_event) => { + match mouse_event { + MouseEvent::Press(termion::event::MouseButton::WheelUp, x, y) => { + // termion is 1-based, ratatui rects are 0-based + let pos = Position { + x: x.saturating_sub(1), + y: y.saturating_sub(1), + }; + + if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { + app.fetch_scroll = app.fetch_scroll.saturating_sub(1); + } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { + app.cook_scroll = app.cook_scroll.saturating_sub(1); + app.cook_auto_scroll = false; + } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(1); + } + } + MouseEvent::Press(termion::event::MouseButton::WheelDown, x, y) => { + let pos = Position { + x: x.saturating_sub(1), + y: y.saturating_sub(1), + }; + + if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { + app.fetch_scroll = app.fetch_scroll.saturating_add(1); + } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { + app.cook_scroll = app.cook_scroll.saturating_add(1); + app.cook_auto_scroll = false; + } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(1); + } + } + _ => {} + } + } + _ => {} + } +} + +fn handle_prompt_input<'a>( + event: &Event, + app: &'a mut TuiApp, +) -> Option<(&'a mut TuiApp, PromptOption)> { + if let Some(prompt) = &mut app.prompt { + match event { + Event::Key(key) => match key { + Key::Char('q') | Key::Ctrl('c') | Key::Esc => { + // Treat as "Exit" + return Some((app, PromptOption::Exit)); + } + Key::Left | Key::BackTab => prompt.prev(), + Key::Right | Key::Char('\t') => prompt.next(), + Key::Char('\n') => { + let prompt = app.prompt.take().unwrap(); + return Some((app, prompt.selected)); + } + _ => {} + }, + _ => {} // Ignore mouse events + } + } + None +} + +fn draw_prompt(f: &mut ratatui::Frame, prompt: &FailurePrompt) { + let title = format!(" FAILURE in {} ", prompt.recipe.name); + let mut error_text = prompt.error.clone(); + if error_text.len() > 200 { + error_text = error_text[0..100].to_string() + + ".." + + &error_text[(error_text.len() - 100)..(error_text.len() - 1)]; + } else if error_text.len() > 100 { + error_text = error_text[0..100].to_string() + ".."; + } + + // Style for options + let retry_style = if prompt.selected == PromptOption::Retry { + Style::default().bg(Color::White).fg(Color::Black) + } else { + Style::default() + }; + let skip_style = if prompt.selected == PromptOption::Skip { + Style::default().bg(Color::White).fg(Color::Black) + } else { + Style::default() + }; + let exit_style = if prompt.selected == PromptOption::Exit { + Style::default().bg(Color::White).fg(Color::Black) + } else { + Style::default() + }; + + let text = vec![ + Line::from(error_text).style(Style::default().fg(Color::Yellow)), + Line::from(""), + Line::from(vec![ + Span::styled(" [Skip] ", skip_style), + Span::raw(" "), + Span::styled(" [Exit] ", exit_style), + Span::raw(" "), + Span::styled(" [Retry] ", retry_style), + ]), + ]; + + let block = Block::default() + .title(Span::styled( + title, + Style::default().fg(Color::White).bg(Color::Red), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)); + + let paragraph = Paragraph::new(text) + .block(block) + .alignment(ratatui::layout::Alignment::Center) + .wrap(Wrap { trim: true }); + + let area = f.area(); + let popup_area = Rect { + x: area.width / 4, + y: area.height / 3, + width: area.width / 2, + height: 10, + }; + + f.render_widget(Clear, popup_area); // Clear the background + f.render_widget(paragraph, popup_area); +} + +fn spawn_log_reader( + mut reader: R, + package_name: PackageName, + status_tx: mpsc::Sender, +) where + R: Read + Send + 'static, +{ + thread::spawn(move || { + let mut buffer = [0; 1024]; + loop { + let buf = match reader.read(&mut buffer) { + Ok(0) => break, + Ok(n) => buffer[..n].to_vec(), + Err(e) => format!("[IO Error] {}", e).into_bytes(), + }; + if status_tx + .send(StatusUpdate::PushLog(package_name.clone(), buf)) + .is_err() + { + // TUI thread hung up + break; + } + } + }); +} + +fn setup_logger( + status_tx: &mpsc::Sender, + name: &PackageName, +) -> (UnixSlavePty, std::io::PipeWriter) { + let (pty_reader, log_reader, pipes) = setup_pty(); + + spawn_log_reader(pty_reader, name.clone(), status_tx.clone()); + spawn_log_reader(log_reader, name.clone(), status_tx.clone()); + pipes +} + +#[derive(PartialEq, Clone, Copy)] +#[repr(u32)] +enum PromptOption { + Retry = 2, + Skip, + Exit, +} + +struct FailurePrompt { + recipe: CookRecipe, + error: String, + selected: PromptOption, +} + +impl FailurePrompt { + fn new(recipe: CookRecipe, error: String) -> Self { + Self { + recipe, + error, + selected: PromptOption::Exit, + } + } + + fn next(&mut self) { + self.selected = match self.selected { + PromptOption::Retry => PromptOption::Skip, + PromptOption::Skip => PromptOption::Exit, + PromptOption::Exit => PromptOption::Retry, + } + } + + fn prev(&mut self) { + self.selected = match self.selected { + PromptOption::Retry => PromptOption::Exit, + PromptOption::Skip => PromptOption::Retry, + PromptOption::Exit => PromptOption::Skip, + } + } +} diff --git a/src/bin/repo_builder.rs b/src/bin/repo_builder.rs index a94123df..4748560e 100644 --- a/src/bin/repo_builder.rs +++ b/src/bin/repo_builder.rs @@ -26,6 +26,9 @@ fn main() -> Result<(), Box> { .next() .expect("Usage: repo_builder ..."); let repo_path = Path::new(&repo_dir); + if !repo_path.is_dir() { + fs::create_dir_all(repo_path)?; + } // Runtime dependencies include both `[package.dependencies]` and dynamically // linked packages discovered by auto_deps. diff --git a/src/config.rs b/src/config.rs index 54d6f837..27e42dd7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,59 @@ -use std::{collections::HashMap, fs, sync::OnceLock}; +use std::{collections::HashMap, env, fs, str::FromStr, sync::OnceLock}; use serde::{Deserialize, Serialize}; +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct CookConfigOpt { + /// whether to run offline + pub offline: Option, + /// whether to set jobs number instead of from nproc + pub jobs: Option, + /// whether to use TUI to allow parallel build + /// default value is yes if "CI" env unset and STDIN is open. + pub tui: Option, + /// whether to ignore build errors + pub nonstop: Option, + /// whether to print success recipes info and warnings + /// build failure still be printed anyway + pub verbose: Option, +} + +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Serialize)] +pub struct CookConfig { + pub offline: bool, + pub jobs: usize, + pub tui: bool, + pub nonstop: bool, + pub verbose: bool, +} + +impl From for CookConfig { + fn from(value: CookConfigOpt) -> Self { + CookConfig { + offline: value.offline.unwrap(), + jobs: value.jobs.unwrap(), + tui: value.tui.unwrap(), + nonstop: value.nonstop.unwrap(), + verbose: value.verbose.unwrap(), + } + } +} + #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(default)] pub struct CookbookConfig { + #[serde(rename = "cook")] + cook_opt: CookConfigOpt, + #[serde(skip)] + pub cook: CookConfig, pub mirrors: HashMap, } static CONFIG: OnceLock = OnceLock::new(); pub fn init_config() { - let config: CookbookConfig = if fs::exists("cookbook.toml").unwrap_or(false) { + let mut config: CookbookConfig = if fs::exists("cookbook.toml").unwrap_or(false) { let toml_content = fs::read_to_string("cookbook.toml") .map_err(|e| format!("Unable to read config: {:?}", e)) .unwrap(); @@ -21,9 +64,44 @@ pub fn init_config() { CookbookConfig::default() }; + if config.cook_opt.tui.is_none() { + config.cook_opt.tui = Some(!env::var("CI").is_ok_and(|s| !s.is_empty())); + } + if config.cook_opt.jobs.is_none() { + config.cook_opt.jobs = Some(extract_env( + "COOKBOOK_MAKE_JOBS", + std::thread::available_parallelism() + .map(|f| usize::from(f)) + .unwrap_or(1), + )); + } + if config.cook_opt.offline.is_none() { + config.cook_opt.offline = Some(extract_env("COOKBOOK_OFFLINE", false)); + } + if config.cook_opt.verbose.is_none() { + config.cook_opt.verbose = Some(extract_env("COOKBOOK_VERBOSE", true)); + } + if config.cook_opt.nonstop.is_none() { + config.cook_opt.nonstop = Some(extract_env("COOKBOOK_NONSTOP", false)); + } + + config.cook = CookConfig::from(config.cook_opt.clone()); + CONFIG.set(config).expect("config is initialized twice"); } +fn extract_env(key: &str, default: T) -> T { + if let Ok(e) = env::var(&key) { + str::parse(&e).unwrap_or(default) + } else { + default + } +} + +pub fn get_config() -> &'static CookbookConfig { + return CONFIG.get().expect("Configuration is not initialized"); +} + pub fn translate_mirror(original_url: &str) -> String { let config = CONFIG.get().expect("Configuration is not initialized"); @@ -75,6 +153,17 @@ mod tests { let _ = CONFIG.set(app_config); } + #[test] + fn test_parse_cook() { + let app_config: CookbookConfig = toml::from_str( + "[cook]\n\ + offline = true\n", + ) + .expect("Unable to parse test config"); + assert_eq!(app_config.cook_opt.offline, Some(true)); + assert_eq!(app_config.cook_opt.jobs, None); + } + #[test] fn test_exact_match() { setup_test_config(); diff --git a/src/cook.rs b/src/cook.rs index 4ffa8a7b..14c752f8 100644 --- a/src/cook.rs +++ b/src/cook.rs @@ -3,4 +3,5 @@ pub mod cook_build; pub mod fetch; pub mod fs; pub mod package; +pub mod pty; pub mod script; diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index bdf7ab88..e379fce6 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -3,6 +3,7 @@ use pkg::{Package, PackageName}; use redoxer::target; use crate::cook::fs::*; +use crate::cook::pty::PtyOut; use crate::cook::script::*; use crate::recipe::AutoDeps; use crate::recipe::BuildKind; @@ -21,9 +22,25 @@ use crate::is_redox; use crate::REMOTE_PKG_SOURCE; +macro_rules! log_warn { + ($logger:expr, $($arg:tt)+) => { + use std::io::Write; + + if $logger.is_some() { + let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( + format!($($arg)+) + .as_bytes(), + ); + } else { + eprintln!($($arg)+); + } + }; +} + fn auto_deps( stage_dir: &Path, dep_pkgars: &BTreeSet<(PackageName, PathBuf)>, + logger: &PtyOut, ) -> BTreeSet { let mut paths = BTreeSet::new(); let mut visited = BTreeSet::new(); @@ -43,7 +60,10 @@ fn auto_deps( }; if visited.contains(&dir) { #[cfg(debug_assertions)] - eprintln!("DEBUG: auto_deps => Skipping `{dir:?}` (already visited)"); + log_warn!( + logger, + "DEBUG: auto_deps => Skipping `{dir:?}` (already visited)" + ); continue; } assert!( @@ -90,7 +110,7 @@ fn auto_deps( continue; }; if let Ok(relative_path) = path.strip_prefix(stage_dir) { - eprintln!("DEBUG: {} needs {}", relative_path.display(), name); + log_warn!(logger, "DEBUG: {} needs {}", relative_path.display(), name); } needed.insert(name.to_string()); } @@ -124,7 +144,7 @@ fn auto_deps( continue; }; if needed.contains(child_name) { - eprintln!("DEBUG: {} provides {}", dep, child_name); + log_warn!(logger, "DEBUG: {} provides {}", dep, child_name); deps.insert(dep.clone()); missing.remove(child_name); } @@ -134,7 +154,7 @@ fn auto_deps( } for name in missing { - eprintln!("WARN: {} missing", name); + log_warn!(logger, "WARN: {} missing", name); } deps @@ -148,9 +168,14 @@ pub fn build( recipe: &Recipe, offline_mode: bool, check_source: bool, + logger: &PtyOut, ) -> Result<(PathBuf, BTreeSet), String> { let sysroot_dir = target_dir.join("sysroot"); let stage_dir = target_dir.join("stage"); + if recipe.build.kind == BuildKind::None { + // metapackages don't need to do anything here + return Ok((stage_dir, BTreeSet::new())); + } let mut dep_pkgars = BTreeSet::new(); for dependency in recipe.build.dependencies.iter() { @@ -169,7 +194,7 @@ pub fn build( } if stage_dir.exists() && !check_source { - let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars)?; + let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars, logger)?; return Ok((stage_dir, auto_deps)); } @@ -185,7 +210,8 @@ pub fn build( if sysroot_dir.is_dir() { let sysroot_modified = modified_dir(&sysroot_dir)?; if sysroot_modified < source_modified || sysroot_modified < deps_modified { - eprintln!( + log_warn!( + logger, "DEBUG: '{}' newer than '{}'", source_dir.display(), sysroot_dir.display() @@ -234,7 +260,8 @@ pub fn build( if stage_dir.is_dir() { let stage_modified = modified_dir(&stage_dir)?; if stage_modified < source_modified || stage_modified < deps_modified { - eprintln!( + log_warn!( + logger, "DEBUG: '{}' newer than '{}'", source_dir.display(), stage_dir.display() @@ -292,7 +319,7 @@ pub fn build( flags_fn("COOKBOOK_MESON_FLAGS", mesonflags), ), BuildKind::Custom { script } => script.clone(), - BuildKind::Remote => return build_remote(target_dir, name, offline_mode), + BuildKind::Remote => return build_remote(target_dir, name, offline_mode, logger), BuildKind::None => "".to_owned(), }; @@ -313,7 +340,7 @@ pub fn build( } else { let cookbook_redoxer = Path::new("target/release/cookbook_redoxer") .canonicalize() - .unwrap(); + .unwrap_or(PathBuf::from("/bin/false")); let mut command = Command::new(&cookbook_redoxer); command.arg("env").arg("bash").arg("-ex"); command.env("COOKBOOK_REDOXER", &cookbook_redoxer); @@ -337,13 +364,13 @@ pub fn build( "{}\n{}\n{}\n{}", BUILD_PRESCRIPT, SHARED_PRESCRIPT, script, BUILD_POSTSCRIPT ); - run_command_stdin(command, full_script.as_bytes())?; + run_command_stdin(command, full_script.as_bytes(), logger)?; // Move stage.tmp to stage atomically rename(&stage_dir_tmp, &stage_dir)?; } - let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars)?; + let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars, logger)?; Ok((stage_dir, auto_deps)) } @@ -353,6 +380,7 @@ fn build_auto_deps( target_dir: &Path, stage_dir: &PathBuf, dep_pkgars: BTreeSet<(PackageName, PathBuf)>, + logger: &PtyOut, ) -> Result, String> { let auto_deps_path = target_dir.join("auto_deps.toml"); if auto_deps_path.is_file() && modified(&auto_deps_path)? < modified(stage_dir)? { @@ -366,7 +394,7 @@ fn build_auto_deps( toml::from_str(&toml_content).map_err(|_| "failed to deserialize cached auto_deps")?; wrapper.packages } else { - let packages = auto_deps(stage_dir, &dep_pkgars); + let packages = auto_deps(stage_dir, &dep_pkgars, logger); let wrapper = AutoDeps { packages }; serialize_and_write(&auto_deps_path, &wrapper)?; wrapper.packages @@ -385,6 +413,7 @@ pub fn build_remote( target_dir: &Path, name: &PackageName, offline_mode: bool, + logger: &PtyOut, ) -> Result<(PathBuf, BTreeSet), String> { // download straight from remote source then declare pkg dependencies as autodeps dependency let stage_dir = target_dir.join("stage"); @@ -394,9 +423,9 @@ pub fn build_remote( let source_pubkey = target_dir.join("id_ed25519.pub.toml"); if !offline_mode { - download_wget(&get_remote_url(name, "pkgar"), &source_pkgar)?; - download_wget(&get_remote_url(name, "toml"), &source_toml)?; - download_wget(&get_pubkey_url(), &source_pubkey)?; + download_wget(&get_remote_url(name, "pkgar"), &source_pkgar, logger)?; + download_wget(&get_remote_url(name, "toml"), &source_toml, logger)?; + download_wget(&get_pubkey_url(), &source_pubkey, logger)?; } else { offline_check_exists(&source_pkgar)?; offline_check_exists(&source_toml)?; @@ -469,7 +498,7 @@ mod tests { "Expected a loop where {dir:?} points to {root:?}" ); - let entries = auto_deps(root, &Default::default()); + let entries = auto_deps(root, &Default::default(), &None); assert!( entries.is_empty(), "auto_deps shouldn't have yielded any libraries" diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index c3ce9bdc..7f569fb5 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -1,13 +1,30 @@ use crate::config::translate_mirror; use crate::cook::fs::*; +use crate::cook::pty::PtyOut; use crate::cook::script::*; use crate::is_redox; +use crate::recipe::BuildKind; use crate::recipe::Recipe; use crate::{blake3, recipe::SourceRecipe}; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +macro_rules! log_warn { + ($logger:expr, $($arg:tt)+) => { + use std::io::Write; + + if $logger.is_some() { + let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( + format!($($arg)+) + .as_bytes(), + ); + } else { + eprintln!($($arg)+); + } + }; +} + pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result { if show_progress { blake3::blake3_progress(&path) @@ -24,14 +41,22 @@ pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result) -> Result { +pub fn fetch_offline( + recipe_dir: &Path, + recipe: &Recipe, + logger: &PtyOut, +) -> Result { let source_dir = recipe_dir.join("source"); - match source { + if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { + // the build function doesn't need source dir exists + return Ok(source_dir); + } + match &recipe.source { Some(SourceRecipe::Path { path: _ }) | None => { - return fetch(recipe_dir, source); + return fetch(recipe_dir, recipe, logger); } Some(SourceRecipe::SameAs { same_as: _ }) => { - return fetch(recipe_dir, source); + return fetch(recipe_dir, recipe, logger); } Some(SourceRecipe::Git { git: _, @@ -52,7 +77,7 @@ pub fn fetch_offline(recipe_dir: &Path, source: &Option) -> Result }) => { if !source_dir.is_dir() { let source_tar = recipe_dir.join("source.tar"); - let source_tar_blake3 = get_blake3(&source_tar, true)?; + let source_tar_blake3 = get_blake3(&source_tar, true && logger.is_none())?; if source_tar.exists() { if let Some(blake3) = blake3 { if source_tar_blake3 != *blake3 { @@ -60,8 +85,8 @@ pub fn fetch_offline(recipe_dir: &Path, source: &Option) -> Result "The downloaded tar blake3 '{source_tar_blake3}' is not equal to blake3 in recipe.toml." )); } - fetch_extract_tar(source_tar, &source_dir)?; - fetch_apply_patches(recipe_dir, patches, script, &source_dir)?; + fetch_extract_tar(source_tar, &source_dir, logger)?; + fetch_apply_patches(recipe_dir, patches, script, &source_dir, logger)?; } else { // need to trust this tar file return Err(format!( @@ -79,18 +104,28 @@ pub fn fetch_offline(recipe_dir: &Path, source: &Option) -> Result Ok(source_dir) } -pub fn fetch(recipe_dir: &Path, source: &Option) -> Result { +pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result { let source_dir = recipe_dir.join("source"); - match source { + if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { + // the build function doesn't need source dir exists + return Ok(source_dir); + } + match &recipe.source { Some(SourceRecipe::SameAs { same_as }) => { - let (canon_dir, recipe) = fetch_resolve_canon(recipe_dir, same_as)?; + let (canon_dir, recipe) = fetch_resolve_canon(recipe_dir, &same_as)?; // recursively fetch - fetch(&canon_dir, &recipe.source)?; - fetch_make_symlink(&source_dir, same_as)?; + fetch(&canon_dir, &recipe, logger)?; + fetch_make_symlink(&source_dir, &same_as)?; } Some(SourceRecipe::Path { path }) => { - if !source_dir.is_dir() || modified_dir(Path::new(path))? > modified_dir(&source_dir)? { - eprintln!("[DEBUG]: {} is newer than {}", path, source_dir.display()); + if !source_dir.is_dir() || modified_dir(Path::new(&path))? > modified_dir(&source_dir)? + { + log_warn!( + logger, + "[DEBUG]: {} is newer than {}", + path, + source_dir.display() + ); copy_dir_all(path, &source_dir).map_err(|e| { format!( "Couldn't copy source from {} to {}: {}", @@ -122,7 +157,7 @@ pub fn fetch(recipe_dir: &Path, source: &Option) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result { if !source_dir.is_dir() { - //TODO: Don't print if build template is none or remote - eprintln!( + log_warn!( + logger, "WARNING: Recipe without source section expected source dir at '{}'", source_dir.display(), ); @@ -333,6 +375,7 @@ pub(crate) fn fetch_resolve_canon( pub(crate) fn fetch_extract_tar( source_tar: PathBuf, source_dir_tmp: &PathBuf, + logger: &PtyOut, ) -> Result<(), String> { let mut command = Command::new("tar"); if is_redox() { @@ -345,7 +388,7 @@ pub(crate) fn fetch_extract_tar( command.arg(&source_tar); command.arg("--directory").arg(source_dir_tmp); command.arg("--strip-components").arg("1"); - run_command(command)?; + run_command(command, logger)?; Ok(()) } @@ -378,6 +421,7 @@ pub(crate) fn fetch_apply_patches( patches: &Vec, script: &Option, source_dir_tmp: &PathBuf, + logger: &PtyOut, ) -> Result<(), String> { for patch_name in patches { let patch_file = recipe_dir.join(patch_name); @@ -400,12 +444,16 @@ pub(crate) fn fetch_apply_patches( let mut command = Command::new("patch"); command.arg("--directory").arg(source_dir_tmp); command.arg("--strip=1"); - run_command_stdin(command, patch.as_bytes())?; + run_command_stdin(command, patch.as_bytes(), logger)?; } Ok(if let Some(script) = script { let mut command = Command::new("bash"); command.arg("-ex"); command.current_dir(source_dir_tmp); - run_command_stdin(command, format!("{SHARED_PRESCRIPT}\n{script}").as_bytes())?; + run_command_stdin( + command, + format!("{SHARED_PRESCRIPT}\n{script}").as_bytes(), + logger, + )?; }) } diff --git a/src/cook/fs.rs b/src/cook/fs.rs index a3295d63..c9385260 100644 --- a/src/cook/fs.rs +++ b/src/cook/fs.rs @@ -8,7 +8,10 @@ use std::{ }; use walkdir::{DirEntry, WalkDir}; -use crate::config::translate_mirror; +use crate::{ + config::translate_mirror, + cook::pty::{PtyOut, spawn_to_pipe}, +}; //TODO: pub(crate) for all of these functions @@ -146,9 +149,10 @@ pub fn rename(src: &Path, dst: &Path) -> Result<(), String> { }) } -pub fn run_command(mut command: process::Command) -> Result<(), String> { - let status = command - .status() +pub fn run_command(mut command: process::Command, stdout_pipe: &PtyOut) -> Result<(), String> { + let status = spawn_to_pipe(&mut command, stdout_pipe) + .map_err(|err| format!("failed to run {:?}: {}\n{:#?}", command, err, err))? + .wait() .map_err(|err| format!("failed to run {:?}: {}\n{:#?}", command, err, err))?; if !status.success() { @@ -161,11 +165,13 @@ pub fn run_command(mut command: process::Command) -> Result<(), String> { Ok(()) } -pub fn run_command_stdin(mut command: process::Command, stdin_data: &[u8]) -> Result<(), String> { +pub fn run_command_stdin( + mut command: process::Command, + stdin_data: &[u8], + stdout_pipe: &PtyOut, +) -> Result<(), String> { command.stdin(Stdio::piped()); - - let mut child = command - .spawn() + let mut child = spawn_to_pipe(&mut command, stdout_pipe) .map_err(|err| format!("failed to spawn {:?}: {}\n{:#?}", command, err, err))?; if let Some(ref mut stdin) = child.stdin { @@ -217,13 +223,13 @@ pub fn offline_check_exists(path: &PathBuf) -> Result<(), String> { Ok(()) } -pub fn download_wget(url: &str, dest: &PathBuf) -> Result<(), String> { +pub fn download_wget(url: &str, dest: &PathBuf, logger: &PtyOut) -> Result<(), String> { if !dest.is_file() { let dest_tmp = PathBuf::from(format!("{}.tmp", dest.display())); let mut command = Command::new("wget"); command.arg(translate_mirror(url)); command.arg("--continue").arg("-O").arg(&dest_tmp); - run_command(command)?; + run_command(command, logger)?; rename(&dest_tmp, &dest)?; } Ok(()) diff --git a/src/cook/package.rs b/src/cook/package.rs index aaee9fa6..fa97817f 100644 --- a/src/cook/package.rs +++ b/src/cook/package.rs @@ -1,13 +1,10 @@ -use std::{ - collections::BTreeSet, - env, - path::{Path, PathBuf}, -}; +use std::{collections::BTreeSet, env, path::Path}; use pkg::{Package, PackageName}; use crate::{ - cook::fs::*, + cook::{fs::*, pty::PtyOut}, + log_to_pty, recipe::{BuildKind, Recipe}, }; @@ -17,7 +14,14 @@ pub fn package( name: &PackageName, recipe: &Recipe, auto_deps: &BTreeSet, -) -> Result { + logger: &PtyOut, +) -> Result<(), String> { + if recipe.build.kind == BuildKind::None { + // metapackages don't have stage dir + package_toml(target_dir, name, recipe, auto_deps)?; + return Ok(()); + } + let secret_path = "build/id_ed25519.toml"; let public_path = "build/id_ed25519.pub.toml"; if !Path::new(secret_path).is_file() || !Path::new(public_path).is_file() { @@ -34,17 +38,20 @@ pub fn package( } let package_file = target_dir.join("stage.pkgar"); + let package_meta = target_dir.join("stage.toml"); // Rebuild package if stage is newer //TODO: rebuild on recipe changes if package_file.is_file() { let stage_modified = modified_dir(stage_dir)?; if modified(&package_file)? < stage_modified { - eprintln!( + log_to_pty!( + logger, "DEBUG: '{}' newer than '{}'", stage_dir.display(), package_file.display() ); remove_all(&package_file)?; + remove_all(&package_meta)?; } } if !package_file.is_file() { @@ -54,11 +61,13 @@ pub fn package( stage_dir.to_str().unwrap(), ) .map_err(|err| format!("failed to create pkgar archive: {:?}", err))?; + } + if !package_meta.is_file() { package_toml(target_dir, name, recipe, auto_deps)?; } - Ok(package_file) + Ok(()) } pub fn package_toml( @@ -80,7 +89,8 @@ pub fn package_toml( depends, }; - serialize_and_write(&target_dir.join("stage.toml"), &package)?; + let toml_path = &target_dir.join("stage.toml"); + serialize_and_write(&toml_path, &package)?; return Ok(()); } diff --git a/src/cook/pty.rs b/src/cook/pty.rs new file mode 100644 index 00000000..96bae8c8 --- /dev/null +++ b/src/cook/pty.rs @@ -0,0 +1,339 @@ +use anyhow::{Error, bail}; +use filedescriptor::FileDescriptor; +use libc::{self, winsize}; +use std::io::Read; +use std::os::fd::FromRawFd; +use std::os::unix::io::AsRawFd; +use std::os::unix::process::CommandExt; +use std::process::Child; +use std::{io, mem, ptr}; +use std::{ + io::{PipeReader, PipeWriter}, + process::Command, +}; + +pub use std::os::unix::io::RawFd; + +#[macro_export] +macro_rules! log_to_pty { + ($logger:expr, $($arg:tt)+) => { + use std::io::Write; + + if $logger.is_some() { + let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( + format!($($arg)+) + .as_bytes(), + ); + } else { + eprintln!($($arg)+); + } + }; +} + +pub type PtyOut<'a> = Option<(&'a mut UnixSlavePty, &'a mut PipeWriter)>; + +pub fn setup_pty() -> ( + Box, + PipeReader, + (UnixSlavePty, std::io::PipeWriter), +) { + let pty_system = UnixPtySystem::default(); + let pair = pty_system + .openpty(PtySize { + rows: 24, // Standard terminal size + cols: 80, // Standard terminal size + ..Default::default() + }) + .expect("Unable to open pty"); + + // TODO: There's no way to handle stdin + let pty_reader = pair + .master + .try_clone_reader() + .expect("Unable to clone pty reader"); + + let (log_reader, log_writer) = std::io::pipe().expect("Failed to create log pipe"); + let pipes = (pair.slave, log_writer); + (pty_reader, log_reader, pipes) +} + +pub fn spawn_to_pipe(command: &mut Command, stdout_pipe: &PtyOut) -> Result { + match stdout_pipe { + Some(stdout) => stdout.0.spawn_command(command.into()), + None => Ok(command.spawn()?), + } +} + +// +// based on portable-pty crate +// copied here since it isn't flexible enough +// + +#[derive(Default)] +pub struct UnixPtySystem {} + +/// Represents the size of the visible display area in the pty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PtySize { + /// The number of lines of text + pub rows: u16, + /// The number of columns of text + pub cols: u16, + /// The width of a cell in pixels. Note that some systems never + /// fill this value and ignore it. + pub pixel_width: u16, + /// The height of a cell in pixels. Note that some systems never + /// fill this value and ignore it. + pub pixel_height: u16, +} + +impl Default for PtySize { + fn default() -> Self { + PtySize { + rows: 24, + cols: 80, + pixel_width: 0, + pixel_height: 0, + } + } +} + +fn openpty(size: PtySize) -> anyhow::Result<(UnixMasterPty, UnixSlavePty)> { + let mut master: RawFd = -1; + let mut slave: RawFd = -1; + + let mut size = winsize { + ws_row: size.rows, + ws_col: size.cols, + ws_xpixel: size.pixel_width, + ws_ypixel: size.pixel_height, + }; + + let result = unsafe { + // BSDish systems may require mut pointers to some args + #[allow(clippy::unnecessary_mut_passed)] + libc::openpty( + &mut master, + &mut slave, + ptr::null_mut(), + ptr::null_mut(), + &mut size, + ) + }; + + if result != 0 { + bail!("failed to openpty: {:?}", io::Error::last_os_error()); + } + + let master = UnixMasterPty { + fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(master) }), + }; + let slave = UnixSlavePty { + fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(slave) }), + }; + + // Ensure that these descriptors will get closed when we execute + // the child process. This is done after constructing the Pty + // instances so that we ensure that the Ptys get drop()'d if + // the cloexec() functions fail (unlikely!). + cloexec(master.fd.as_raw_fd())?; + cloexec(slave.fd.as_raw_fd())?; + + Ok((master, slave)) +} + +pub struct PtyPair { + // slave is listed first so that it is dropped first. + // The drop order is stable and specified by rust rfc 1857 + pub slave: UnixSlavePty, + pub master: UnixMasterPty, +} + +impl UnixPtySystem { + fn openpty(&self, size: PtySize) -> anyhow::Result { + let (master, slave) = openpty(size)?; + Ok(PtyPair { + master: master, + slave: slave, + }) + } +} + +struct PtyFd(pub FileDescriptor); +impl std::ops::Deref for PtyFd { + type Target = FileDescriptor; + fn deref(&self) -> &FileDescriptor { + &self.0 + } +} +impl std::ops::DerefMut for PtyFd { + fn deref_mut(&mut self) -> &mut FileDescriptor { + &mut self.0 + } +} + +impl Read for PtyFd { + fn read(&mut self, buf: &mut [u8]) -> Result { + match self.0.read(buf) { + Err(ref e) if e.raw_os_error() == Some(libc::EIO) => { + // EIO indicates that the slave pty has been closed. + // Treat this as EOF so that std::io::Read::read_to_string + // and similar functions gracefully terminate when they + // encounter this condition + Ok(0) + } + x => x, + } + } +} + +impl PtyFd { + fn resize(&self, size: PtySize) -> Result<(), Error> { + let ws_size = winsize { + ws_row: size.rows, + ws_col: size.cols, + ws_xpixel: size.pixel_width, + ws_ypixel: size.pixel_height, + }; + + if unsafe { + libc::ioctl( + self.0.as_raw_fd(), + libc::TIOCSWINSZ as _, + &ws_size as *const _, + ) + } != 0 + { + bail!( + "failed to ioctl(TIOCSWINSZ): {:?}", + io::Error::last_os_error() + ); + } + + Ok(()) + } + + fn get_size(&self) -> Result { + let mut size: winsize = unsafe { mem::zeroed() }; + if unsafe { + libc::ioctl( + self.0.as_raw_fd(), + libc::TIOCGWINSZ as _, + &mut size as *mut _, + ) + } != 0 + { + bail!( + "failed to ioctl(TIOCGWINSZ): {:?}", + io::Error::last_os_error() + ); + } + Ok(PtySize { + rows: size.ws_row, + cols: size.ws_col, + pixel_width: size.ws_xpixel, + pixel_height: size.ws_ypixel, + }) + } + + fn spawn_command(&self, cmd: &mut Command) -> anyhow::Result { + unsafe { + cmd + // .stdin(self.as_stdio()?) + .stdout(self.as_stdio()?) + .stderr(self.as_stdio()?) + .pre_exec(move || { + // Clean up a few things before we exec the program + // Clear out any potentially problematic signal + // dispositions that we might have inherited + for signo in &[ + libc::SIGCHLD, + libc::SIGHUP, + libc::SIGINT, + libc::SIGQUIT, + libc::SIGTERM, + libc::SIGALRM, + ] { + libc::signal(*signo, libc::SIG_DFL); + } + + let empty_set: libc::sigset_t = std::mem::zeroed(); + libc::sigprocmask(libc::SIG_SETMASK, &empty_set, std::ptr::null_mut()); + + // Establish ourselves as a session leader. + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } + + Ok(()) + }) + }; + + let mut child = cmd.spawn()?; + + // Ensure that we close out the slave fds that Child retains; + // they are not what we need (we need the master side to reference + // them) and won't work in the usual way anyway. + // In practice these are None, but it seems best to be move them + // out in case the behavior of Command changes in the future. + // child.stdin.take(); + child.stdout.take(); + child.stderr.take(); + + Ok(child) + } +} + +/// Represents the master end of a pty. +/// The file descriptor will be closed when the Pty is dropped. +pub struct UnixMasterPty { + fd: PtyFd, +} + +/// Represents the slave end of a pty. +/// The file descriptor will be closed when the Pty is dropped. +pub struct UnixSlavePty { + fd: PtyFd, +} + +/// Helper function to set the close-on-exec flag for a raw descriptor +fn cloexec(fd: RawFd) -> Result<(), Error> { + let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; + if flags == -1 { + bail!( + "fcntl to read flags failed: {:?}", + io::Error::last_os_error() + ); + } + let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) }; + if result == -1 { + bail!( + "fcntl to set CLOEXEC failed: {:?}", + io::Error::last_os_error() + ); + } + Ok(()) +} + +impl UnixSlavePty { + fn spawn_command(&self, builder: &mut Command) -> Result { + Ok(self.fd.spawn_command(builder)?) + } +} + +impl UnixMasterPty { + #[allow(unused)] + fn resize(&self, size: PtySize) -> Result<(), Error> { + self.fd.resize(size) + } + + #[allow(unused)] + fn get_size(&self) -> Result { + self.fd.get_size() + } + + fn try_clone_reader(&self) -> Result, Error> { + let fd = PtyFd(self.fd.try_clone()?); + Ok(Box::new(fd)) + } +} diff --git a/src/recipe.rs b/src/recipe.rs index 99e80800..98ca4360 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -1,4 +1,9 @@ -use std::{collections::BTreeSet, convert::TryInto, fs, path::PathBuf}; +use std::{ + collections::BTreeSet, + convert::TryInto, + fs, + path::{Path, PathBuf}, +}; use pkg::{PackageName, package::PackageError, recipes}; use regex::Regex; @@ -10,7 +15,7 @@ use serde::{ use crate::WALK_DEPTH; /// Specifies how to download the source for a recipe -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(untagged)] pub enum SourceRecipe { /// Reuse the source directory of another package @@ -83,7 +88,7 @@ impl SourceRecipe { } /// Specifies how to build a recipe -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(tag = "template")] pub enum BuildKind { /// Will not build (for meta packages) @@ -129,7 +134,7 @@ impl Default for BuildKind { } } -#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct BuildRecipe { #[serde(flatten, default)] pub kind: BuildKind, @@ -137,7 +142,7 @@ pub struct BuildRecipe { pub dependencies: Vec, } -#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct PackageRecipe { #[serde(default)] pub dependencies: Vec, @@ -146,7 +151,7 @@ pub struct PackageRecipe { } /// Everything required to build a Redox package -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct Recipe { /// Specifies how to download the source for this recipe pub source: Option, @@ -158,7 +163,19 @@ pub struct Recipe { pub package: PackageRecipe, } -#[derive(Debug, PartialEq)] +impl Recipe { + pub fn new(file: &PathBuf) -> Result { + if !file.is_file() { + return Err(PackageError::FileMissing(file.clone())); + } + let toml = fs::read_to_string(&file) + .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?; + let recipe: Recipe = toml::from_str(&toml) + .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?; + Ok(recipe) + } +} +#[derive(Debug, Clone, PartialEq)] pub struct CookRecipe { pub name: PackageName, pub dir: PathBuf, @@ -168,24 +185,7 @@ pub struct CookRecipe { } impl CookRecipe { - pub fn new( - name: impl TryInto, - ) -> Result { - let name: PackageName = name.try_into()?; - let dir = recipes::find(name.as_str()) - .ok_or_else(|| PackageError::PackageNotFound(name.clone()))?; - let file = dir.join("recipe.toml"); - if !file.is_file() { - return Err(PackageError::FileMissing(file)); - } - - let toml = fs::read_to_string(&file) - .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?; - - let recipe: Recipe = toml::from_str(&toml) - .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file)))?; - - let dir = dir.to_path_buf(); + pub fn new(name: PackageName, dir: PathBuf, recipe: Recipe) -> Result { Ok(Self { name, dir, @@ -194,6 +194,29 @@ impl CookRecipe { }) } + pub fn from_name( + name: impl TryInto, + ) -> Result { + let name: PackageName = name.try_into()?; + let dir = recipes::find(name.as_str()) + .ok_or_else(|| PackageError::PackageNotFound(name.clone()))?; + let file = dir.join("recipe.toml"); + let recipe = Recipe::new(&file)?; + Self::new(name, dir.to_path_buf(), recipe) + } + + pub fn from_path(dir: &Path, read_recipe: bool) -> Result { + let file = dir.join("recipe.toml"); + let name: PackageName = dir.file_name().unwrap().try_into()?; + let recipe = if read_recipe { + Recipe::new(&file)? + } else { + // clean/unfetch don't need to read recipe + Recipe::default() + }; + Self::new(name, dir.to_path_buf(), recipe) + } + pub fn new_recursive( names: &[PackageName], recursion: usize, @@ -204,7 +227,7 @@ impl CookRecipe { let mut recipes = Vec::new(); for name in names { - let recipe = Self::new(name.as_str())?; + let recipe = Self::from_name(name.as_str())?; let dependencies = Self::new_recursive(&recipe.recipe.build.dependencies, recursion - 1).map_err( @@ -253,7 +276,7 @@ impl CookRecipe { let mut recipes: Vec = Vec::new(); for name in names { - let recipe = Self::new(name.as_str())?; + let recipe = Self::from_name(name.as_str())?; let dependencies = Self::get_package_deps_recursive( &recipe.recipe.package.dependencies, diff --git a/unfetch.sh b/unfetch.sh index ba273e87..4b7b6262 100755 --- a/unfetch.sh +++ b/unfetch.sh @@ -1,20 +1,13 @@ #!/usr/bin/env bash set -e -source config.sh +source `dirname "$0"`/config.sh if [ $# = 0 ] then - recipes="$(list_recipes --short)" + recipes="--all" else recipes="$@" fi -for recipe_name in $recipes -do - recipe_path=`find_recipe $recipe_name` - - echo -e "\033[01;38;5;215mcook - unfetch $recipe_name\033[0m" - rm -rfv "$recipe_path"/source "$recipe_path"/source.tar -done - +repo unfetch $recipes