From bfd529097f52ba9a269083e58b62d16baf47ee89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Wed, 14 Aug 2024 18:34:19 +0100 Subject: [PATCH] WIP tasks --- Cargo.lock | 407 +++++++++++++++++++--- devenv.nix | 6 + devenv/Cargo.toml | 11 + devenv/src/cli.rs | 13 + devenv/src/devenv.rs | 37 +- devenv/src/lib.rs | 3 + devenv/src/main.rs | 9 +- devenv/src/tasks.rs | 716 ++++++++++++++++++++++++++++++++++++++ docs/.pages | 1 + docs/tasks.md | 53 +++ src/modules/tasks.nix | 101 ++++++ src/modules/top-level.nix | 1 + tests/tasks/.gitignore | 2 + tests/tasks/devenv.nix | 19 + 14 files changed, 1326 insertions(+), 53 deletions(-) create mode 100644 devenv/src/tasks.rs create mode 100644 docs/tasks.md create mode 100644 src/modules/tasks.nix create mode 100644 tests/tasks/.gitignore create mode 100644 tests/tasks/devenv.nix diff --git a/Cargo.lock b/Cargo.lock index a71c075f5..1d7b58874 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,6 +83,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "autocfg" version = "1.1.0" @@ -154,9 +160,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "castaway" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ "rustversion", ] @@ -314,6 +320,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.4.2", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -385,8 +416,10 @@ name = "devenv" version = "1.0.8" dependencies = [ "ansiterm", + "assert_matches", "clap", "cli-table", + "crossterm", "dotlock", "fs2", "hex", @@ -394,6 +427,7 @@ dependencies = [ "indoc", "miette", "nix", + "petgraph", "regex", "reqwest", "schemars", @@ -403,6 +437,10 @@ dependencies = [ "serde_yaml", "sha2", "tempdir", + "tempfile", + "test-log", + "thiserror", + "tokio", "tracing", "which", "whoami", @@ -479,6 +517,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -501,6 +560,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fnv" version = "1.0.7" @@ -678,6 +743,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex" version = "0.4.3" @@ -863,7 +934,7 @@ checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ "bitflags 2.4.2", "libc", - "redox_syscall", + "redox_syscall 0.4.1", ] [[package]] @@ -872,11 +943,21 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "markdown" @@ -887,6 +968,15 @@ dependencies = [ "unicode-id", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.1" @@ -941,13 +1031,15 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi", "libc", + "log", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -980,6 +1072,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "object" version = "0.32.2" @@ -1045,18 +1147,57 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owo-colors" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.3", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1139,6 +1280,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.4.2", +] + [[package]] name = "redox_users" version = "0.4.4" @@ -1158,8 +1308,17 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1170,9 +1329,15 @@ checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.2" @@ -1242,9 +1407,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.4.2", "errno", @@ -1349,6 +1514,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3adfbe1c90a6a9643433e490ef1605c6a99f93be37e4c83fe5149fca9698c6" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "2.9.2" @@ -1461,6 +1632,45 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.9" @@ -1603,14 +1813,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1632,6 +1843,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "test-log" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dffced63c2b5c7be278154d76b479f9f9920ed34e7574201407f0b14e2bbb93" +dependencies = [ + "env_logger", + "test-log-macros", + "tracing-subscriber", +] + +[[package]] +name = "test-log-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5999e24eaa32083191ba4e425deb75cdf25efefabe5aaccb7446dd0d4122a3f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.51", +] + [[package]] name = "textwrap" version = "0.16.1" @@ -1645,24 +1878,34 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", "syn 2.0.51", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1680,17 +1923,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", - "windows-sys 0.48.0", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.51", ] [[package]] @@ -1752,6 +2008,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1807,9 +2092,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unsafe-libyaml" @@ -1834,6 +2119,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1962,7 +2253,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "redox_syscall", + "redox_syscall 0.4.1", "wasite", "web-sys", ] @@ -2013,7 +2304,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -2033,17 +2333,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.3", - "windows_aarch64_msvc 0.52.3", - "windows_i686_gnu 0.52.3", - "windows_i686_msvc 0.52.3", - "windows_x86_64_gnu 0.52.3", - "windows_x86_64_gnullvm 0.52.3", - "windows_x86_64_msvc 0.52.3", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -2054,9 +2355,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -2066,9 +2367,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -2078,9 +2379,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.3" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -2090,9 +2397,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -2102,9 +2409,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -2114,9 +2421,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -2126,9 +2433,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winreg" diff --git a/devenv.nix b/devenv.nix index 420eded57..a528f5baf 100644 --- a/devenv.nix +++ b/devenv.nix @@ -1,5 +1,7 @@ { inputs, pkgs, lib, config, ... }: { env.DEVENV_NIX = inputs.nix.packages.${pkgs.stdenv.system}.nix; + env.RUST_LOG = "devenv=debug"; + env.RUST_LOG_SPAN_EVENTS = "full"; packages = [ pkgs.cairo @@ -208,6 +210,10 @@ EOF ''; }; + + tasks.sleep.exec = "sleep 10"; + tasks.sleep.depends = [ "enterShell" ]; + pre-commit.hooks = { nixpkgs-fmt.enable = true; #shellcheck.enable = true; diff --git a/devenv/Cargo.toml b/devenv/Cargo.toml index 52eebe899..e05528bdd 100644 --- a/devenv/Cargo.toml +++ b/devenv/Cargo.toml @@ -7,8 +7,10 @@ license.workspace = true [dependencies] ansiterm.workspace = true +assert_matches = "1.5.0" clap.workspace = true cli-table.workspace = true +crossterm = "0.28.1" dotlock.workspace = true fs2.workspace = true hex.workspace = true @@ -16,6 +18,7 @@ include_dir.workspace = true indoc.workspace = true miette.workspace = true nix.workspace = true +petgraph = "0.6.5" regex.workspace = true reqwest.workspace = true schemars.workspace = true @@ -25,6 +28,14 @@ serde_json.workspace = true serde_yaml.workspace = true sha2.workspace = true tempdir.workspace = true +tempfile = "3.12.0" +test-log = { version = "0.2.16", features = ["trace"] } +thiserror = "1.0.63" +tokio = { version = "1.39.3", features = [ + "process", + "macros", + "rt-multi-thread", +] } tracing.workspace = true which.workspace = true whoami.workspace = true diff --git a/devenv/src/cli.rs b/devenv/src/cli.rs index a9ecb0999..a6d1ef503 100644 --- a/devenv/src/cli.rs +++ b/devenv/src/cli.rs @@ -165,6 +165,12 @@ pub(crate) enum Commands { command: ProcessesCommand, }, + #[command(about = "Run tasks. https://devenv.sh/tasks/")] + Tasks { + #[command(subcommand)] + command: TasksCommand, + }, + #[command(about = "Run tests. http://devenv.sh/tests/", alias = "ci")] Test { #[arg(short, long, help = "Don't override .devenv to a temporary directory.")] @@ -241,6 +247,13 @@ pub(crate) enum ProcessesCommand { // TODO: Status/Attach } +#[derive(Subcommand, Clone)] +#[clap(about = "Run tasks. https://devenv.sh/tasks/")] +pub(crate) enum TasksCommand { + #[command(about = "Run tasks.")] + Run { tasks: Vec }, +} + #[derive(Subcommand, Clone)] #[clap( about = "Build, copy, or run a container. https://devenv.sh/containers/", diff --git a/devenv/src/devenv.rs b/devenv/src/devenv.rs index ed92a815e..ed22338a3 100644 --- a/devenv/src/devenv.rs +++ b/devenv/src/devenv.rs @@ -1,4 +1,4 @@ -use super::{cli, command, config, log}; +use super::{cli, command, config, log, tasks}; use clap::crate_version; use cli_table::Table; use cli_table::{print_stderr, WithTitle}; @@ -566,6 +566,41 @@ impl Devenv { Ok(self.has_processes.unwrap()) } + pub async fn tasks_run(&mut self, roots: Vec) -> Result<()> { + if roots.is_empty() { + bail!("No tasks specified."); + } + let config = { + let _logprogress = self.log_progress.with_newline("Evaluating tasks"); + self.run_nix( + "nix", + &[ + "build", + ".#devenv.task.config", + "--no-link", + "--print-out-paths", + ], + &command::Options::default(), + )? + }; + // parse tasks config + let config_content = + std::fs::read_to_string(String::from_utf8_lossy(&config.stdout).trim()) + .expect("Failed to read config file"); + let tasks: Vec = + serde_json::from_str(&config_content).expect("Failed to parse tasks config"); + // run tasks + let config = tasks::Config { roots, tasks }; + let mut tui = tasks::TasksUi::new(config).await?; + let tasks_status = tui.run().await?; + + if tasks_status.failed > 0 || tasks_status.dependency_failed > 0 { + Err(miette::bail!("Some tasks failed")) + } else { + Ok(()) + } + } + pub fn test(&mut self) -> Result<()> { self.assemble(true)?; diff --git a/devenv/src/lib.rs b/devenv/src/lib.rs index 1274afaa5..aa2c14302 100644 --- a/devenv/src/lib.rs +++ b/devenv/src/lib.rs @@ -1,8 +1,11 @@ +#[macro_use] +extern crate assert_matches; mod cli; pub mod command; pub mod config; mod devenv; pub mod log; +pub mod tasks; pub use cli::{default_system, GlobalOptions}; pub use devenv::{Devenv, DevenvOptions}; diff --git a/devenv/src/main.rs b/devenv/src/main.rs index 483cedf9a..7093bb656 100644 --- a/devenv/src/main.rs +++ b/devenv/src/main.rs @@ -3,13 +3,15 @@ mod command; mod config; mod devenv; mod log; +mod tasks; use clap::{crate_version, Parser}; -use cli::{Cli, Commands, ContainerCommand, InputsCommand, ProcessesCommand}; +use cli::{Cli, Commands, ContainerCommand, InputsCommand, ProcessesCommand, TasksCommand}; use devenv::Devenv; use miette::Result; -fn main() -> Result<()> { +#[tokio::main] +async fn main() -> Result<()> { let cli = Cli::parse(); let level = if cli.global_options.verbose { @@ -126,6 +128,9 @@ fn main() -> Result<()> { } ProcessesCommand::Down {} => devenv.down(), }, + Commands::Tasks { command } => match command { + TasksCommand::Run { tasks } => devenv.tasks_run(tasks).await, + }, Commands::Inputs { command } => match command { InputsCommand::Add { name, url, follows } => devenv.inputs_add(&name, &url, &follows), }, diff --git a/devenv/src/tasks.rs b/devenv/src/tasks.rs new file mode 100644 index 000000000..e9ca9cffa --- /dev/null +++ b/devenv/src/tasks.rs @@ -0,0 +1,716 @@ +use assert_matches::assert_matches; +use crossterm::{ + cursor, execute, + style::{self, Stylize}, + terminal::{Clear, ClearType}, +}; +use miette::Diagnostic; +use petgraph::algo::toposort; +use petgraph::graph::{DiGraph, NodeIndex}; +use petgraph::visit::{Dfs, EdgeRef}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::io::{self, Write}; +use std::process::Stdio; +use std::sync::Arc; +use std::{ + collections::{HashMap, HashSet}, + fs, +}; +use std::{fmt::Display, os::unix::fs::PermissionsExt}; +use test_log::test; +use thiserror::Error; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio::sync::{Mutex, RwLock}; +use tokio::task::JoinSet; +use tokio::time::{Duration, Instant}; +use tracing::{error, info, instrument}; + +#[derive(Error, Diagnostic, Debug)] +pub enum Error { + #[error(transparent)] + IoError(#[from] std::io::Error), + TaskNotFound(String), + TasksNotFound(Vec<(String, String)>), + InvalidTaskName(String), + // TODO: be more precies where the cycle happens + CycleDetected(String), +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::IoError(e) => write!(f, "IO Error: {}", e), + Error::TasksNotFound(tasks) => write!( + f, + "Task dependencies not found: {}", + tasks + .iter() + .map(|(task, dep)| format!("{} is depending on non-existent {}", task, dep)) + .collect::>() + .join(", ") + ), + Error::TaskNotFound(task) => write!(f, "Task does not exist: {}", task), + Error::CycleDetected(task) => write!(f, "Cycle detected at task: {}", task), + Error::InvalidTaskName(task) => write!( + f, + "Invalid task name: {}, expected [a-zA-Z-_]:[a-zA-Z-_]", + task + ), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct TaskConfig { + name: String, + #[serde(default)] + depends: Vec, + #[serde(default)] + command: Option, + #[serde(default)] + status: Option, +} + +#[derive(Deserialize)] +pub struct Config { + pub tasks: Vec, + pub roots: Vec, +} + +impl TryFrom for Config { + type Error = serde_json::Error; + + fn try_from(json: serde_json::Value) -> Result { + serde_json::from_value(json) + } +} + +#[derive(Debug)] +enum TaskCompleted { + Success, + Failed, + Skipped, + DependencyFailed, +} + +#[derive(Debug)] +enum TaskStatus { + Pending, + Running(Instant), + Completed((Duration, TaskCompleted)), +} + +#[derive(Debug)] +struct TaskState { + task: TaskConfig, + status: TaskStatus, +} + +impl TaskState { + fn new(task: TaskConfig) -> Self { + Self { + task, + status: TaskStatus::Pending, + } + } + + #[instrument] + async fn run(&mut self) -> TaskCompleted { + if let Some(status) = &self.task.status { + let mut child = Command::new(status) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to execute status"); + + match child.wait().await { + Err(_) => {} + Ok(status) => { + if status.success() { + return TaskCompleted::Skipped; + } + } + } + } + if let Some(cmd) = &self.task.command { + let mut child = Command::new(cmd) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to execute command"); + + let stdout = child.stdout.take().expect("Failed to open stdout"); + let stderr = child.stderr.take().expect("Failed to open stderr"); + + let mut stderr_reader = BufReader::new(stderr).lines(); + let mut stdout_reader = BufReader::new(stdout).lines(); + + loop { + tokio::select! { + result = stdout_reader.next_line() => { + match result { + Ok(Some(line)) => info!(stdout = %line), + Ok(None) => break, + Err(e) => error!("Error reading stdout: {}", e), + } + } + result = stderr_reader.next_line() => { + match result { + Ok(Some(line)) => error!(stderr = %line), + Ok(None) => break, + Err(e) => error!("Error reading stderr: {}", e), + } + } + result = child.wait() => { + match result { + Ok(status) => { + if status.success() { + return TaskCompleted::Success; + } else { + return TaskCompleted::Failed; + } + }, + Err(e) => { + error!("Error waiting for command: {}", e); + return TaskCompleted::Failed; + } + } + } + } + } + } + return TaskCompleted::Skipped; + } +} + +#[derive(Debug)] +struct Tasks { + roots: Vec, + sender_tx: Sender, + graph: DiGraph>, ()>, + tasks_order: Vec, +} + +impl Tasks { + async fn new(config: Config) -> Result<(Self, Receiver), Error> { + let (sender_tx, receiver_rx) = channel(1000); + let mut graph = DiGraph::new(); + let mut task_indices = HashMap::new(); + for task in config.tasks { + let name = task.name.clone(); + if !task.name.contains(':') + || task.name.split(':').count() != 2 + || task.name.starts_with(':') + || task.name.ends_with(':') + || !task + .name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == ':' || c == '_' || c == '-') + { + return Err(Error::InvalidTaskName(name)); + } + let index = graph.add_node(Arc::new(RwLock::new(TaskState::new(task)))); + task_indices.insert(name, index); + } + let mut roots = Vec::new(); + for name in config.roots { + if let Some(index) = task_indices.get(&name) { + roots.push(*index); + } else { + return Err(Error::TaskNotFound(name)); + } + } + let mut tasks = Self { + roots, + sender_tx, + graph, + tasks_order: vec![], + }; + tasks.resolve_dependencies(task_indices).await?; + tasks.schedule().await?; + Ok((tasks, receiver_rx)) + } + + async fn resolve_dependencies( + &mut self, + task_indices: HashMap, + ) -> Result<(), Error> { + let mut unresolved = HashSet::new(); + let mut edges_to_add = Vec::new(); + + for index in self.graph.node_indices() { + let task_state = &self.graph[index].read().await; + + for dep_name in &task_state.task.depends { + if let Some(dep_idx) = task_indices.get(dep_name) { + edges_to_add.push((*dep_idx, index)); + } else { + unresolved.insert((task_state.task.name.clone(), dep_name.clone())); + } + } + } + + for (dep_idx, idx) in edges_to_add { + self.graph.add_edge(dep_idx, idx, ()); + } + + if unresolved.is_empty() { + Ok(()) + } else { + Err(Error::TasksNotFound(unresolved.into_iter().collect())) + } + } + + #[instrument(skip(self))] + async fn schedule(&mut self) -> Result<(), Error> { + // TODO: we traverse the graph twice, see https://github.com/petgraph/petgraph/issues/661 + let mut subgraph = DiGraph::new(); + + // Map to track which nodes in the original graph correspond to which nodes in the new subgraph + let mut node_map = HashMap::new(); + let mut visited = HashSet::new(); + + // Traverse the graph starting from the root nodes + for root_index in &self.roots { + let mut dfs = Dfs::new(&self.graph, *root_index); + + while let Some(node) = dfs.next(&self.graph) { + if visited.insert(node) { + // Add the node to the new subgraph and map it + let new_node = subgraph.add_node(self.graph[node].clone()); + node_map.insert(node, new_node); + + // Copy edges to the new subgraph + for edge in self.graph.edges(node) { + let target = edge.target(); + if visited.contains(&target) { + // Both nodes must already be added to subgraph + let new_source = node_map[&node]; + let new_target = node_map[&target]; + subgraph.add_edge(new_source, new_target, ()); + } + } + } + } + } + + self.graph = subgraph; + + match toposort(&self.graph, None) { + Ok(indexes) => { + self.tasks_order = indexes; + Ok(()) + } + Err(cycle) => Err(Error::CycleDetected( + self.graph[cycle.node_id()].read().await.task.name.clone(), + )), + } + } + + #[instrument(skip(self))] + async fn run(&mut self) -> Result<(), Error> { + let mut running_tasks = JoinSet::new(); + + for index in &self.tasks_order { + let task_state = &self.graph[*index]; + + loop { + let mut dependencies_completed = true; + for dep_index in self + .graph + .neighbors_directed(*index, petgraph::Direction::Outgoing) + { + match self.graph[dep_index].read().await.status { + TaskStatus::Completed(_) => {} + TaskStatus::Pending => { + dependencies_completed = false; + break; + } + TaskStatus::Running(_) => { + dependencies_completed = false; + break; + } + } + } + + if dependencies_completed { + break; + } + + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + + let task_state_clone = Arc::clone(task_state); + + running_tasks.spawn(async move { + let mut task_state = task_state_clone.write().await; + let now = Instant::now(); + task_state.status = TaskStatus::Running(now); + let completed = task_state.run().await; + task_state.status = TaskStatus::Completed((now.elapsed(), completed)); + }); + } + + while let Some(res) = running_tasks.join_next().await { + match res { + Ok(_) => (), + Err(e) => eprintln!("Task failed: {:?}", e), + } + } + + Ok(()) + } +} + +struct TaskUpdate { + name: String, + status: TaskStatus, +} + +pub struct TasksStatus { + lines: Vec, + pub pending: usize, + pub running: usize, + pub succeeded: usize, + pub failed: usize, + pub skipped: usize, + pub dependency_failed: usize, +} + +impl TasksStatus { + fn new() -> Self { + Self { + lines: vec![], + pending: 0, + running: 0, + succeeded: 0, + failed: 0, + skipped: 0, + dependency_failed: 0, + } + } +} + +pub struct TasksUi { + tasks: Arc>, + receiver_rx: Receiver, +} + +impl TasksUi { + pub async fn new(config: Config) -> Result { + let (tasks, receiver_rx) = Tasks::new(config).await?; + Ok(Self { + tasks: Arc::new(Mutex::new(tasks)), + receiver_rx, + }) + } + + async fn get_tasks_status(&self) -> TasksStatus { + let mut tasks_status = TasksStatus::new(); + let tasks = self.tasks.lock().await; + + for index in &tasks.tasks_order { + let task_state = tasks.graph[*index].read().await; + let (status_text, duration) = match &task_state.status { + TaskStatus::Pending => { + tasks_status.pending += 1; + continue; + } + TaskStatus::Running(started) => { + tasks_status.running += 1; + ("Running".blue().bold(), Some(started.elapsed())) + } + TaskStatus::Completed((duration, TaskCompleted::Skipped)) => { + tasks_status.skipped += 1; + ("Skipped".blue().bold(), None) + } + TaskStatus::Completed((duration, TaskCompleted::Success)) => { + tasks_status.succeeded += 1; + ("Succeeded".green().bold(), Some(*duration)) + } + TaskStatus::Completed((duration, TaskCompleted::Failed)) => { + tasks_status.failed += 1; + ("Failed".red().bold(), Some(*duration)) + } + TaskStatus::Completed((duration, TaskCompleted::DependencyFailed)) => { + tasks_status.dependency_failed += 1; + ("Dependency failed".magenta().bold(), Some(*duration)) + } + }; + + let duration = match duration { + Some(d) => d.as_millis().to_string() + "ms", + None => "".to_string(), + }; + tasks_status.lines.push(format!( + "{} {} {}", + status_text, &task_state.task.name, duration + )); + } + + tasks_status + } + + pub async fn run(&mut self) -> Result { + // start processing tasks + let tasks_clone = Arc::clone(&self.tasks); + let handle = tokio::spawn(async move { + let mut tasks = tasks_clone.lock().await; + if let Err(e) = tasks.run().await { + eprintln!("Error running tasks: {:?}", e); + } + }); + + // start TUI + let mut stdout = io::stdout(); + let mut last_list_height: u16 = 0; + + loop { + let tasks_status = self.get_tasks_status().await; + + execute!( + stdout, + // Clear the screen from the cursor down + cursor::MoveUp(last_list_height), + Clear(ClearType::FromCursorDown), + style::PrintStyledContent( + format!( + "{}\nTasks: {}\n", + tasks_status.lines.join("\n"), + [ + if tasks_status.pending > 0 { + format!("{} {}", "Pending".blue().bold(), tasks_status.pending) + } else { + String::new() + }, + if tasks_status.running > 0 { + format!("{} {}", "Running".blue().bold(), tasks_status.running) + } else { + String::new() + }, + if tasks_status.skipped > 0 { + format!("{} {}", "Skipped".blue().bold(), tasks_status.skipped) + } else { + String::new() + }, + if tasks_status.succeeded > 0 { + format!("{} {}", "Succeeded".green().bold(), tasks_status.succeeded) + } else { + String::new() + }, + if tasks_status.failed > 0 { + format!("{} {}", "Failed".red().bold(), tasks_status.failed) + } else { + String::new() + }, + if tasks_status.dependency_failed > 0 { + format!( + "{} {}", + "Dependency Failed".red().bold(), + tasks_status.dependency_failed + ) + } else { + String::new() + }, + ] + .into_iter() + .filter(|s| !s.is_empty()) + .collect::>() + .join(", ") + ) + .stylize() + ) + )?; + + last_list_height = tasks_status.lines.len() as u16 + 1; + + if handle.is_finished() { + break; + } + + // Sleep briefly to avoid excessive redraws + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + + let tasks_status = self.get_tasks_status().await; + Ok(tasks_status) + } +} + +#[test(tokio::test)] +async fn test_task_name() -> Result<(), Error> { + let invalid_names = vec![ + "invalid:name!", + "invalid name", + "invalid@name", + ":invalid", + "invalid:", + "invalid", + ]; + for task in invalid_names { + assert_matches!( + Config::try_from(json!({ + "roots": [], + "tasks": [{ + "name": task.to_string() + }] + })) + .map(Tasks::new) + .unwrap() + .await, + Err(Error::InvalidTaskName(_)) + ); + } + let valid_names = vec![ + "devenv:enterShell", + "devenv:enter-shell", + "devenv:enter_shell", + ]; + for task in valid_names { + assert_matches!( + Config::try_from(serde_json::json!({ + "roots": [], + "tasks": [{ + "name": task.to_string() + }] + })) + .map(Tasks::new) + .unwrap() + .await, + Ok(_) + ); + } + Ok(()) +} + +#[test(tokio::test)] +async fn test_basic_tasks() -> Result<(), Error> { + let script1 = + create_script("#!/bin/sh\necho 'Task 1 is running' && sleep 2 && echo 'Task 1 completed'")?; + let script2 = + create_script("#!/bin/sh\necho 'Task 2 is running' && sleep 3 && echo 'Task 2 completed'")?; + let script3 = + create_script("#!/bin/sh\necho 'Task 3 is running' && sleep 1 && echo 'Task 3 completed'")?; + let script4 = create_script("#!/bin/sh\necho 'Task 4 is running' && echo 'Task 4 completed'")?; + + let (mut tasks, _) = Tasks::new( + Config::try_from(json!({ + "roots": ["myapp:task_1", "myapp:task_4"], + "tasks": [ + { + "name": "myapp:task_1", + "command": script1.to_str().unwrap() + }, + { + "name": "myapp:task_2", + "command": script2.to_str().unwrap() + }, + { + "name": "myapp:task_3", + "depends": ["myapp:task_1"], + "command": script3.to_str().unwrap() + }, + { + "name": "myapp:task_4", + "depends": ["myapp:task_3"], + "command": script4.to_str().unwrap() + } + ] + })) + .unwrap(), + ) + .await?; + tasks.run().await?; + Ok(()) +} + +// #[test(tokio::test)] +// async fn test_tasks_cycle() -> Result<(), Error> { +// let (mut tasks, _) = Tasks::new( +// Config::try_from(json!({ +// "roots": ["myapp:task_1"], +// "tasks": [ +// { +// "name": "myapp:task_1", +// "depends": ["myapp:task_2"], +// "command": "echo 'Task 1 is running' && sleep 2 && echo 'Task 1 completed'" +// }, +// { +// "name": "myapp:task_2", +// "depends": ["myapp:task_1"], +// "command": "echo 'Task 2 is running' && sleep 3 && echo 'Task 2 completed'" +// } +// ] +// })) +// .unwrap(), +// ) +// .await?; + +// let err = "myapp_task_2".to_string(); + +// assert!(matches!(tasks.run().await, Err(Error::CycleDetected(err)))); +// Ok(()) +// } + +#[test(tokio::test)] +async fn test_status() -> Result<(), Error> { + let run_task = |root: &'static str| async move { + let command_script1 = + create_script("#!/bin/sh\necho 'Task 1 is running' && echo 'Task 1 completed'")?; + let status_script1 = create_script("#!/bin/sh\nexit 0")?; + let command_script2 = + create_script("#!/bin/sh\necho 'Task 2 is running' && echo 'Task 2 completed'")?; + let status_script2 = create_script("#!/bin/sh\nexit 1")?; + + Tasks::new( + Config::try_from(json!({ + "roots": [root], + "tasks": [ + { + "name": "myapp:task_1", + "command": command_script1.to_str().unwrap(), + "status": status_script1.to_str().unwrap() + }, + { + "name": "myapp:task_2", + "command": command_script2.to_str().unwrap(), + "status": status_script2.to_str().unwrap() + } + ] + })) + .unwrap(), + ) + .await + }; + + let (mut tasks, _) = run_task("myapp:task_1").await.unwrap(); + tasks.run().await?; + assert_matches!( + tasks.graph[tasks.tasks_order[0]].read().await.status, + TaskStatus::Completed((_, TaskCompleted::Skipped)) + ); + + let (mut tasks, _) = run_task("myapp:task_2").await.unwrap(); + tasks.run().await?; + assert_matches!( + tasks.graph[tasks.tasks_order[0]].read().await.status, + TaskStatus::Completed((_, TaskCompleted::Success)) + ); + + Ok(()) +} + +fn create_script(script: &str) -> std::io::Result { + let mut temp_file = tempfile::Builder::new() + .prefix(&format!("script")) + .suffix(".sh") + .tempfile()?; + temp_file.write_all(script.as_bytes())?; + temp_file + .as_file_mut() + .set_permissions(fs::Permissions::from_mode(0o755))?; + Ok(temp_file.into_temp_path()) +} diff --git a/docs/.pages b/docs/.pages index a8a96f2b3..28de28969 100644 --- a/docs/.pages +++ b/docs/.pages @@ -7,6 +7,7 @@ nav: - Basics: basics.md - Packages: packages.md - Scripts: scripts.md + - Tasks: tasks.md - Languages: - Overview: languages.md - Supported Languages: supported-languages diff --git a/docs/tasks.md b/docs/tasks.md new file mode 100644 index 000000000..cf97a7746 --- /dev/null +++ b/docs/tasks.md @@ -0,0 +1,53 @@ +Tasks allow you to form dependencies between commands, executed in parallel. + +## Defining tasks + +```nix title="devenv.nix" +{ pkgs, ... }: + +{ + tasks."myapp:hello" = { + exec = ''echo "Hello, world!"''; + desc = "hello world in bash"; + }; +} +``` + +```shell-session +$ devenv tasks run hello +• Building shell ... +• Entering shell ... +Hello, world! +$ +``` + +## Using your favourite language + +Tasks can also reference scripts and depend on other tasks, for example when entering the shell: + +```nix title="devenv.nix" +{ pkgs, lib, config, ... }: + +{ + tasks = { + "python:hello"" = { + exec = ''print("Hello world from Python!")''; + package = config.languages.python.package; + }; + "bash:hello" = { + exec = "echo 'Hello world from bash!'"; + depends = [ "python:hello" ]; + }; + enterShell.depends = [ "bash:hello" ]; + }; +} +``` + +```shell-session +$ devenv shell +• Building shell ... +• Entering shell ... +Hello world from Python! +Hello world from bash! +$ +``` diff --git a/src/modules/tasks.nix b/src/modules/tasks.nix new file mode 100644 index 000000000..5eb840e5b --- /dev/null +++ b/src/modules/tasks.nix @@ -0,0 +1,101 @@ +{ pkgs, lib, config, ... }: +let + types = lib.types; + taskType = types.submodule + ({ name, config, ... }: + let + mkCommand = command: + if builtins.isNull command + then null + else + pkgs.writeScriptBin name '' + #!${pkgs.lib.getBin config.package}/bin/${config.binary} + ${command} + ''; + in + { + options = { + exec = lib.mkOption { + type = types.nullOr types.str; + default = null; + description = "Command to execute the task."; + }; + binary = lib.mkOption { + type = types.str; + description = "Override the binary name if it doesn't match package name"; + default = config.package.pname; + }; + package = lib.mkOption { + type = types.nullOr types.package; + default = null; + description = "Package to install for this task."; + }; + command = lib.mkOption { + type = types.nullOr types.package; + internal = true; + default = mkCommand config.exec; + description = "Path to the script to run."; + }; + status = lib.mkOption { + type = types.nullOr types.str; + default = null; + description = "Check if the command should be ran"; + }; + statusCommand = lib.mkOption { + type = types.nullOr types.package; + internal = true; + default = mkCommand config.exec; + description = "Path to the script to run."; + }; + config = lib.mkOption { + type = types.attrsOf types.anything; + internal = true; + default = { + name = name; + description = config.description; + status = config.statusCommand; + depends = config.depends; + command = config.command; + }; + }; + description = lib.mkOption { + type = types.str; + default = ""; + description = "Description of the task."; + }; + depends = lib.mkOption { + type = types.listOf types.str; + description = "List of tasks to run before this task."; + default = [ ]; + }; + }; + }); +in +{ + options.tasks = lib.mkOption { + type = types.attrsOf taskType; + }; + + options.task.config = lib.mkOption { + type = types.package; + internal = true; + default = (pkgs.formats.json { }).generate "tasks.json" (lib.mapAttrsToList (name: value: { inherit name; } // value) config.tasks); + }; + + config = { + info.infoSections.tasks = + lib.mapAttrsToList + (name: task: "${name}: ${task.description} ${task.command}") + config.tasks; + tasks = { + "devenv:enterShell" = { + description = "Runs when entering the shell"; + }; + "devenv:enterTest" = { + description = "Runs when entering the test environment"; + }; + }; + #enterShell = "devenv tasks run devenv:enterShell"; + #enterTest = "devenv tasks run devenv:enterTest"; + }; +} diff --git a/src/modules/top-level.nix b/src/modules/top-level.nix index edf4b361c..5dce1edf6 100644 --- a/src/modules/top-level.nix +++ b/src/modules/top-level.nix @@ -216,6 +216,7 @@ in ./lib.nix ./tests.nix ./cachix.nix + ./tasks.nix ] ++ (listEntries ./languages) ++ (listEntries ./services) diff --git a/tests/tasks/.gitignore b/tests/tasks/.gitignore new file mode 100644 index 000000000..c75670085 --- /dev/null +++ b/tests/tasks/.gitignore @@ -0,0 +1,2 @@ +shell +test diff --git a/tests/tasks/devenv.nix b/tests/tasks/devenv.nix new file mode 100644 index 000000000..9bbe1a812 --- /dev/null +++ b/tests/tasks/devenv.nix @@ -0,0 +1,19 @@ +{ + tasks = { + shell.exec = "touch shell"; + enterShell.depends = [ "shell" ]; + test.exec = "touch test"; + }; + + enterTest = '' + if [ ! -f shell ]; then + echo "shell does not exist" + exit 1 + fi + devenv tasks run test + if [ ! -f test ]; then + echo "test does not exist" + exit 1 + fi + ''; +}