diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d18bd21 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2023-03-12 + +Initial release. + +### Added + +- Hot reloading for [Native](https://github.com/solana-labs/solana), [Anchor](https://github.com/coral-xyz/anchor) and [Seahorse](https://github.com/ameliatastic/seahorse-lang). + +[unreleased]: https://github.com/acheroncrypto/watchso/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/acheroncrypto/watchso/releases/tag/v0.1.0 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0aa5142 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1786 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-priority-channel" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c21678992e1b21bebfe2bc53ab5f5f68c106eddab31b24e0bb06e9b715a86640" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-recursion" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b015a331cc64ebd1774ba119538573603427eaace0a1950c423ab971f903796" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-take" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f0778972c64420fdedc63f09919c8a88bda7b25135357fd25a5d9f3257e832" +dependencies = [ + "memchr", + "once_cell", + "regex-automata", + "serde", +] + +[[package]] +name = "btoi" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c0869a9faa81f8bbf8102371105d6d0a7b79167a04c340b04ab16892246a11" +dependencies = [ + "num-traits", +] + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cargo_toml" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f83bc2e401ed041b7057345ebc488c005efa0341d5541ce7004d30458d0090b" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clearscreen" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41aa24cc5e1d6b3fc49ad4cd540b522fedcbe88bc6f259ff16e20e7010b6f8c7" +dependencies = [ + "nix", + "terminfo", + "thiserror", + "which", + "winapi", +] + +[[package]] +name = "command-group" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "026c3922235f9f7d78f21251a026f3acdeb7cce3deba107fe09a4bfa63d850a2" +dependencies = [ + "async-trait", + "nix", + "tokio", + "winapi", +] + +[[package]] +name = "console" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.42.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.45.0", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" + +[[package]] +name = "futures-executor" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" + +[[package]] +name = "futures-macro" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" + +[[package]] +name = "futures-task" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" + +[[package]] +name = "futures-util" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + +[[package]] +name = "git-actor" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599cb75eb7c3bf03149b7ab70c66bc0b9a432a092b1154b49d21b49fc7e4bd93" +dependencies = [ + "bstr", + "btoi", + "git-date", + "itoa", + "nom 7.1.3", + "quick-error", +] + +[[package]] +name = "git-config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1b95089db62159d7d24c13ddfb4bf949c508521d0bb25331744ef500295ceb8" +dependencies = [ + "bstr", + "git-config-value", + "git-features", + "git-glob", + "git-path", + "git-ref", + "git-sec", + "memchr", + "nom 7.1.3", + "once_cell", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "git-config-value" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989a90c1c630513a153c685b4249b96fdf938afc75bf7ef2ae1ccbd3d799f5db" +dependencies = [ + "bitflags", + "bstr", + "git-path", + "libc", + "thiserror", +] + +[[package]] +name = "git-date" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2874ce2f3a77cb144167901ea830969e5c991eac7bfee85e6e3f53ef9fcdf2" +dependencies = [ + "bstr", + "itoa", + "thiserror", + "time", +] + +[[package]] +name = "git-features" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0019327672cb759f851d1b18fdcc36bb797dc62b925cb93c8c881b54735eb2c2" +dependencies = [ + "git-hash", + "libc", + "sha1_smol", + "walkdir", +] + +[[package]] +name = "git-glob" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa73cf9c9c1a66e28de1cf250fc1ebe323e7c7c59768c1a2331e3b3308e783a3" +dependencies = [ + "bitflags", + "bstr", +] + +[[package]] +name = "git-hash" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1532d82bf830532f8d545c5b7b568e311e3593f16cf7ee9dd0ce03c74b12b99d" +dependencies = [ + "hex", + "thiserror", +] + +[[package]] +name = "git-lock" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7cf6a3c9d1a9932bb9bcb7e0044e2e429f9d94711969a7d2a09e34ae21f6437" +dependencies = [ + "fastrand", + "git-tempfile", + "quick-error", +] + +[[package]] +name = "git-object" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c2ddb376b99172dc8b651e11be4cb49cef423de4fad563ebbda4fee3fcf6" +dependencies = [ + "bstr", + "btoi", + "git-actor", + "git-features", + "git-hash", + "git-validate", + "hex", + "itoa", + "nom 7.1.3", + "smallvec", + "thiserror", +] + +[[package]] +name = "git-path" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40e68481a06da243d3f4dfd86a4be39c24eefb535017a862e845140dcdb878a" +dependencies = [ + "bstr", + "thiserror", +] + +[[package]] +name = "git-ref" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b351af399166a5506369e36389d6b9aee744cfa672d0e2ea93d2b01223b1cfbe" +dependencies = [ + "git-actor", + "git-features", + "git-hash", + "git-lock", + "git-object", + "git-path", + "git-tempfile", + "git-validate", + "memmap2", + "nom 7.1.3", + "thiserror", +] + +[[package]] +name = "git-sec" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6696a816445a51f76995d579a3122f98247377cc45cd681764f740f3a2666004" +dependencies = [ + "bitflags", + "dirs", + "git-path", + "libc", + "windows", +] + +[[package]] +name = "git-tempfile" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d851911a2b043dc1ab6cd5432ce7a3ee3a2fd614ed87428cec1b15f5abb7e0c" +dependencies = [ + "dashmap", + "libc", + "once_cell", + "signal-hook", + "signal-hook-registry", + "tempfile", +] + +[[package]] +name = "git-validate" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431cf9352c596dc7c8ec9066ee551ce54e63c86c3c767e5baf763f6019ff3c2" +dependencies = [ + "bstr", + "thiserror", +] + +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "ignore-files" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f6fe1437ef5a520e79d63958e6bfb7cfd26e30d15e4e29d3d5561697099a70" +dependencies = [ + "futures", + "git-config", + "ignore", + "miette", + "project-origins", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "kqueue" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmap2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd9b301defa984bbdbe112b4763e093ed191750a0d914a78c1106b2d0fe703" +dependencies = [ + "atty", + "backtrace", + "miette-derive", + "once_cell", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c2401ab7ac5282ca5c8b518a87635b1a93762b0b90b9990c509888eeccba29" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.42.0", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", + "static_assertions", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[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 = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + +[[package]] +name = "normalize-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf22e319b2e3cb517350572e3b70c6822e0a520abfb5c78f690e829a73e8d9f2" + +[[package]] +name = "notify" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9" +dependencies = [ + "bitflags", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "windows-sys 0.42.0", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "object" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "project-origins" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629e0d57f265ca8238345cb616eea8847b8ecb86b5d97d155be2c8963a314379" +dependencies = [ + "futures", + "tokio", + "tokio-stream", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "supports-color" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f" +dependencies = [ + "atty", + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590b34f7c5f01ecc9d78dba4b3f445f31df750a67621cf31626f3b7441ce6406" +dependencies = [ + "atty", +] + +[[package]] +name = "supports-unicode" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8b945e45b417b125a8ec51f1b7df2f8df7920367700d1f98aedd21e5735f8b2" +dependencies = [ + "atty", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "terminfo" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da31aef70da0f6352dbcb462683eb4dd2bfad01cf3fc96cf204547b9a839a585" +dependencies = [ + "dirs", + "fnv", + "nom 5.1.2", + "phf", + "phf_codegen", +] + +[[package]] +name = "textwrap" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +dependencies = [ + "itoa", + "libc", + "num_threads", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] + +[[package]] +name = "tokio" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys 0.42.0", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5" +dependencies = [ + "indexmap", + "nom8", + "serde", + "serde_spanned", + "toml_datetime", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-bom" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63ec69f541d875b783ca40184d655f2927c95f0bffd486faa83cd3ac3529ec32" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-linebreak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +dependencies = [ + "hashbrown", + "regex", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "watchexec" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a10297dd0e440aef63b0cc507c71b37ecd29a6892ad690379f6dde562041fd95" +dependencies = [ + "async-priority-channel", + "async-recursion", + "atomic-take", + "clearscreen", + "command-group", + "futures", + "ignore-files", + "miette", + "nix", + "normalize-path", + "notify", + "once_cell", + "project-origins", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "watchexec-filterer-globset" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769f4a7fcf1357a87bd981921630d773d2a7ed493e89c3659063c5aaca87a8b2" +dependencies = [ + "ignore", + "ignore-files", + "tracing", + "watchexec", + "watchexec-filterer-ignore", +] + +[[package]] +name = "watchexec-filterer-ignore" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951cae5c54f929d3346b6a8eb0abf9a648cc41a32198aca7c1dd4f6b2aaf4d9" +dependencies = [ + "ignore", + "ignore-files", + "tracing", + "watchexec", +] + +[[package]] +name = "watchso" +version = "0.1.0" +dependencies = [ + "async-recursion", + "async-trait", + "cargo_toml", + "console", + "globset", + "indicatif", + "lazy_static", + "miette", + "regex", + "thiserror", + "tokio", + "toml", + "watchexec", + "watchexec-filterer-globset", +] + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d03cb9d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "watchso" +version = "0.1.0" +authors = ["Acheron "] +description = "Hot reload Solana programs" +edition = "2021" +repository = "https://github.com/acheroncrypto/watchso" +homepage = "https://github.com/acheroncrypto/watchso" +license = "Apache-2.0" +readme = "README.md" +keywords = ["watch", "hot", "reloading", "solana", "development"] +categories = ["command-line-utilities", "development-tools", "filesystem"] + +[[bin]] +name = "watchso" +path = "src/bin/main.rs" + +[lib] +path = "src/lib/lib.rs" + +[dependencies] +async-recursion = "1.0.2" +async-trait = "0.1.64" +cargo_toml = "0.15.2" +console = "0.15.5" +globset = "0.4.10" +indicatif = "0.17.3" +lazy_static = "1.4.0" +miette = { version = "5.5.0", features = ["fancy"] } +regex = "1.7.1" +thiserror = "1.0.38" +tokio = "1.25.0" +toml = "0.7.2" +watchexec = "2.1.1" +watchexec-filterer-globset = "1.1.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..905aed4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2023 acheroncrypto + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..72396f0 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# watchso + +[![Crates.io](https://img.shields.io/crates/v/watchso.svg)](https://crates.io/crates/watchso) [![Documentation](https://docs.rs/watchso/badge.svg)](https://docs.rs/watchso/) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/acheroncrypto/watchso/blob/master/LICENSE) + +Hot reload [Solana](https://solana.com) programs. + +## Installation + +Install with [cargo](https://www.rust-lang.org/learn/get-started): + +```sh +cargo install watchso --locked +``` + +## Usage + +Run in the directory of your project: + +```sh +watchso +``` + +This will: + +1. Check whether the necessary tools are installed e.g [solana-cli-tools](https://docs.solana.com/cli/install-solana-cli-tools). +2. Start a Solana test validator if it's not already running. +3. Update program id(s) if there is a mismatch between the keypair files and the source code. +4. Build the program(s). +5. Deploy the program(s). +6. Hot reload on changes. + +### Supported frameworks + +- [Native Solana](https://github.com/solana-labs/solana) +- [Anchor](https://github.com/coral-xyz/anchor) +- [Seahorse](https://github.com/ameliatastic/seahorse-lang) + +## License + +[Apache-2.0](https://github.com/acheroncrypto/watchso/blob/master/LICENSE) diff --git a/src/bin/frameworks/anchor.rs b/src/bin/frameworks/anchor.rs new file mode 100644 index 0000000..2a6ff36 --- /dev/null +++ b/src/bin/frameworks/anchor.rs @@ -0,0 +1,82 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use async_trait::async_trait; +use watchso::{ + command::WCommand, + error::WatchError, + framework::{Framework, WatchableFramework}, + framework_utils::{get_program_name_path_hashmap, ProjectMap}, +}; + +#[derive(Default)] +pub struct Anchor { + /// Starting directory path + origin: Arc, + /// Map of program names and paths + project_map: ProjectMap, +} + +impl Anchor { + pub fn new>(origin: P) -> Self { + Self { + origin: Arc::new(origin.as_ref().to_path_buf()), + ..Default::default() + } + } +} + +/// Default implementation works. +impl WatchableFramework for Anchor {} + +#[async_trait] +impl Framework for Anchor { + fn origin(&self) -> &Path { + self.origin.as_path() + } + + async fn check_toolset(&self) -> miette::Result<()> { + const ANCHOR: &str = "anchor"; + if !WCommand::exists(ANCHOR).await { + Err(WatchError::CommandNotFound(ANCHOR))? + } + + Ok(()) + } + + async fn map_program_names(&self) -> miette::Result<()> { + for (name, path) in get_program_name_path_hashmap(self.origin()).await? { + self.project_map.set_program_path(name, path).await; + } + + Ok(()) + } + + async fn get_program_path(&self, path: &Path) -> Option { + self.project_map.get_program_path(path).await + } + + async fn build(&self, program_path: &Path) -> WCommand { + // Changing the current directory to the program's path makes Anchor build only the + // modified program in the workspace. + let mut command = WCommand::new("anchor build"); + command.current_dir(program_path); + command + } + + async fn deploy(&self, elf_path: &Path) -> WCommand { + // Anchor still deploys all of the programs in the workspace even after changing the + // current dir to the program's dir and it is using program dirname as program name + // instead of manifest's package name. Thus, we get the program name from the dirname + // and only deploy the modified program. + self.get_program_path(elf_path) + .await + .as_ref() + .and_then(|path| path.file_name()) + .and_then(|name| name.to_str()) + .map(|name| WCommand::new(format!("anchor deploy -p {name}"))) + .unwrap_or(WCommand::new("anchor deploy")) + } +} diff --git a/src/bin/frameworks/mod.rs b/src/bin/frameworks/mod.rs new file mode 100644 index 0000000..1257082 --- /dev/null +++ b/src/bin/frameworks/mod.rs @@ -0,0 +1,47 @@ +mod anchor; +mod native; +mod seahorse; + +use std::{path::Path, sync::Arc}; + +use miette::IntoDiagnostic; +use tokio::fs; +use watchso::{ + constants::{dirname, filename}, + error::WatchError, + framework::WatchableFramework, +}; + +use self::{anchor::Anchor, native::Native, seahorse::Seahorse}; + +/// Get a [`WatchableFramework`] from the given path. +/// +/// Returns [WatchError::InvalidProgramDirectory] error if the given path is not a valid Solana +/// program directory. +pub async fn get_framework_from_path>( + origin: P, +) -> miette::Result> { + let mut item_names = vec![]; + + let mut dir = fs::read_dir(&origin).await.into_diagnostic()?; + while let Some(entry) = dir.next_entry().await.into_diagnostic()? { + item_names.push(entry.file_name()); + } + + let item_names = item_names + .iter() + .filter_map(|item| item.to_str()) + .collect::>(); + + if item_names.contains(&dirname::PROGRAMS_PY) { + return Ok(Arc::new(Seahorse::new(origin))); + } + if item_names.contains(&filename::ANCHOR_TOML) { + return Ok(Arc::new(Anchor::new(origin))); + } + if item_names.contains(&filename::CARGO_TOML) { + return Ok(Arc::new(Native::new(origin))); + } + + Err(WatchError::InvalidProgramDirectory(origin.as_ref().into()))? +} diff --git a/src/bin/frameworks/native.rs b/src/bin/frameworks/native.rs new file mode 100644 index 0000000..a498235 --- /dev/null +++ b/src/bin/frameworks/native.rs @@ -0,0 +1,91 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use async_trait::async_trait; +use tokio::sync::RwLock; +use watchso::{ + command::WCommand, + framework::{Framework, WatchableFramework}, + framework_utils::{get_bpf_or_sbf, get_program_name_path_hashmap, ProjectMap}, +}; + +#[derive(Default)] +pub struct Native { + /// Starting directory path + origin: Arc, + /// Map of program names and paths + project_map: ProjectMap, + // Full build command to run. Either `cargo build-bpf` or `cargo build-sbf` + build_cmd: BuildCommand, +} + +impl Native { + pub fn new>(origin: P) -> Self { + Self { + origin: Arc::new(origin.as_ref().to_path_buf()), + ..Default::default() + } + } +} + +/// Default implementation works. +impl WatchableFramework for Native {} + +#[async_trait] +impl Framework for Native { + fn origin(&self) -> &Path { + self.origin.as_path() + } + + async fn check_toolset(&self) -> miette::Result<()> { + let build_cmd = get_bpf_or_sbf().await?; + self.build_cmd.set(build_cmd).await; + + Ok(()) + } + + async fn map_program_names(&self) -> miette::Result<()> { + for (name, path) in get_program_name_path_hashmap(self.origin()).await? { + self.project_map.set_program_path(name, path).await; + } + + Ok(()) + } + + async fn get_program_path(&self, path: &Path) -> Option { + self.project_map.get_program_path(path).await + } + + async fn build(&self, program_path: &Path) -> WCommand { + let mut command = WCommand::new(self.build_cmd.get().await); + command.current_dir(program_path); + command + } + + async fn deploy(&self, elf_path: &Path) -> WCommand { + WCommand::new(format!("solana program deploy {}", elf_path.display())) + } +} + +/// Full build command to run. Using `RwLock` because the process is read heavy. +struct BuildCommand(Arc>); + +impl BuildCommand { + /// Get the current build command. + async fn get(&self) -> &'static str { + *self.0.read().await + } + + /// Set the current build command. + async fn set(&self, build_cmd: &'static str) { + *self.0.write().await = build_cmd; + } +} + +impl Default for BuildCommand { + fn default() -> Self { + Self(Arc::new(RwLock::new("cargo build-sbf"))) + } +} diff --git a/src/bin/frameworks/seahorse.rs b/src/bin/frameworks/seahorse.rs new file mode 100644 index 0000000..a56c2e2 --- /dev/null +++ b/src/bin/frameworks/seahorse.rs @@ -0,0 +1,175 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use async_trait::async_trait; +use lazy_static::lazy_static; +use regex::{Regex, RegexBuilder}; +use watchexec::filter::Filterer; +use watchso::{ + action::WAction, + command::WCommand, + constants::{dirname, extension}, + error::WatchError, + framework::{Framework, WatchableFramework}, + framework_utils::{ + create_globset_filterer, get_pubkey_from_keypair_path, update_file_program_id_with, + ProjectMap, + }, + glob::glob, +}; + +#[derive(Default)] +pub struct Seahorse { + /// Starting directory path + origin: Arc, + /// Map of program names and paths + project_map: ProjectMap, +} + +impl Seahorse { + pub fn new>(origin: P) -> Self { + Self { + origin: Arc::new(origin.as_ref().to_path_buf()), + ..Default::default() + } + } +} + +#[async_trait] +impl WatchableFramework for Seahorse { + async fn pathset(&self) -> miette::Result> { + let paths = vec![ + Path::new(dirname::TARGET).join(dirname::DEPLOY), + PathBuf::from(dirname::PROGRAMS_PY), + ]; + + Ok(paths) + } + + async fn filterer(&self) -> Arc { + let filters = []; + let ignores = []; + let extensions = [extension::PY, extension::SO, extension::JSON]; + + create_globset_filterer(self.origin(), &filters, &ignores, &extensions).await + } + + async fn on_action(&self, action: WAction) -> miette::Result<()> { + for action_path in action.get_unique_paths() { + if let Some(ext) = action_path.extension().and_then(|ext| ext.to_str()) { + match ext { + extension::PY => { + self.build(action_path).await.spawn().await?; + } + extension::SO => { + self.deploy(action_path).await.spawn().await?; + } + extension::JSON => { + self.update_program_id(action_path).await?; + } + _ => (), + } + } + } + + Ok(()) + } +} + +#[async_trait] +impl Framework for Seahorse { + fn origin(&self) -> &Path { + self.origin.as_path() + } + + async fn check_toolset(&self) -> miette::Result<()> { + const SEAHORSE: &str = "seahorse"; + if !WCommand::exists(SEAHORSE).await { + Err(WatchError::CommandNotFound(SEAHORSE))? + } + + Ok(()) + } + + async fn map_program_names(&self) -> miette::Result<()> { + let paths = glob( + self.origin().join(dirname::PROGRAMS_PY), + [format!("*.{}", extension::PY)], + [], + true, + ) + .await?; + + for path in paths { + if let Some(program_name) = get_program_name_from_path(&path) { + self.project_map + .set_program_path(program_name.to_owned(), path) + .await; + } + } + + Ok(()) + } + + async fn get_program_path(&self, path: &Path) -> Option { + self.project_map.get_program_path(path).await + } + + async fn update_program_id(&self, program_keypair_path: &Path) -> miette::Result<()> { + if let Some(program_path) = self.get_program_path(program_keypair_path).await { + let program_id = get_pubkey_from_keypair_path(program_keypair_path).await?; + update_seahorse_program_id(program_path, program_id).await?; + } + + Ok(()) + } + + async fn build(&self, program_path: &Path) -> WCommand { + match get_program_name_from_path(program_path) { + Some(program_name) => WCommand::new(format!("seahorse build -p {program_name}")), + None => WCommand::new("seahorse build"), + } + } + + async fn deploy(&self, elf_path: &Path) -> WCommand { + self.get_program_path(elf_path) + .await + .as_ref() + .and_then(|path| path.file_name()) + .and_then(|name| name.to_str()) + .map(|name| WCommand::new(format!("anchor deploy -p {name}"))) + .unwrap_or(WCommand::new("anchor deploy")) + } +} + +/// Get program name from program's path. +/// +/// Seahorse generates Anchor programs based on program's Python file name. +fn get_program_name_from_path(path: &Path) -> Option<&str> { + path.file_name() + .and_then(|name| name.to_str()) + .map(|name| name.trim_end_matches(".py")) +} + +/// Update the file at the given path's `declare_id` function with the given program id. +/// +/// Returns whether the program id was updated successfully. +async fn update_seahorse_program_id(path: P, program_id: S) -> miette::Result +where + P: AsRef, + S: AsRef, +{ + lazy_static! { + static ref REGEX: Regex = RegexBuilder::new(r#"^declare_id\(("|')(\w*)("|')\)"#) + .multi_line(true) + .build() + .unwrap(); + }; + + update_file_program_id_with(path, &program_id, |content| { + REGEX.captures(content).and_then(|captures| captures.get(2)) + }) + .await +} diff --git a/src/bin/main.rs b/src/bin/main.rs new file mode 100644 index 0000000..9d1f5c0 --- /dev/null +++ b/src/bin/main.rs @@ -0,0 +1,14 @@ +mod frameworks; + +use std::env; + +use frameworks::get_framework_from_path; +use miette::IntoDiagnostic; +use watchso::watch; + +#[tokio::main] +async fn main() -> miette::Result<()> { + let origin = env::current_dir().into_diagnostic()?; + let framework = get_framework_from_path(origin).await?; + watch(framework).await +} diff --git a/src/lib/action.rs b/src/lib/action.rs new file mode 100644 index 0000000..f00c0eb --- /dev/null +++ b/src/lib/action.rs @@ -0,0 +1,48 @@ +//! Utilities for [`Action`]. + +use std::{collections::HashSet, path::Path}; + +use watchexec::{action::Action, event::Event, signal::source::MainSignal}; + +/// Utility struct for [`Action`]. +pub struct WAction(Action); + +impl WAction { + /// Create a new [`WAction`]. + pub fn new(action: Action) -> Self { + Self(action) + } + + /// Return the internal action by consuming `self`. + pub fn take(self) -> Action { + self.0 + } + + /// Returns whether the action includes [`MainSignal::Interrupt`]. + pub fn is_interrupt(&self) -> bool { + self.is_any_signal(MainSignal::Interrupt) + } + + /// Returns whether the action includes [`MainSignal::Terminate`]. + pub fn is_terminate(&self) -> bool { + self.is_any_signal(MainSignal::Terminate) + } + + /// Get all the unique paths in the action event paths. + pub fn get_unique_paths(&self) -> HashSet<&Path> { + let mut hashset = HashSet::new(); + for (path, _) in self.0.events.iter().flat_map(Event::paths) { + hashset.insert(path); + } + hashset + } + + /// Returns whether any signal includes the given signal in the events list. + fn is_any_signal(&self, signal: MainSignal) -> bool { + self.0 + .events + .iter() + .flat_map(Event::signals) + .any(|sig| sig == signal) + } +} diff --git a/src/lib/command.rs b/src/lib/command.rs new file mode 100644 index 0000000..61eb72c --- /dev/null +++ b/src/lib/command.rs @@ -0,0 +1,92 @@ +//! Utilities for commands. + +use std::{ + fmt::Display, + path::Path, + process::{ExitStatus, Output}, +}; + +use miette::IntoDiagnostic; +use tokio::process::Command; + +/// Utility struct for [`Command`]. +pub struct WCommand(Command); + +impl WCommand { + /// Create a new [`WCommand`]. + pub fn new>(cmd: C) -> Self { + let cmd_words = cmd.as_ref().split_whitespace().collect::>(); + let mut cmd = Command::new(cmd_words[0]); + cmd.args(&cmd_words[1..]); + + Self(cmd) + } + + /// Set the current directory of the command. + pub fn current_dir>(&mut self, dir: D) -> &mut Self { + self.0.current_dir(dir); + self + } + + /// Get the output of the command. + pub async fn output(&mut self) -> miette::Result { + self.0 + .output() + .await + .into_diagnostic() + .map(|output| output.into()) + } + + /// Spawn the command. + /// + /// Returns the exit status of the command. + pub async fn spawn(&mut self) -> miette::Result { + self.0 + .spawn() + .into_diagnostic()? + .wait() + .await + .into_diagnostic() + .map(|status| status.success()) + } + + /// Returns whether the given command is installed. + pub async fn exists(cmd: D) -> bool { + Self::new(format!("{cmd} --version")) + .output() + .await + .map(|output| output.status().success()) + .unwrap_or(false) + } +} + +/// Utility struct for [`Output`]. +pub struct ReadableOutput(Output); + +impl ReadableOutput { + /// Get the exit status of the output. + pub fn status(&self) -> ExitStatus { + self.0.status + } + + /// Get the UTF-8 converted stderr. + pub fn stderr(&self) -> &str { + Self::convert(&self.0.stderr) + } + + /// Get the UTF-8 converted stdout. + pub fn stdout(&self) -> &str { + Self::convert(&self.0.stdout) + } + + /// Convert the given bytes to UTF-8. + fn convert(bytes: &[u8]) -> &str { + std::str::from_utf8(bytes).unwrap_or_default() + } +} + +impl From for ReadableOutput { + fn from(output: Output) -> Self { + Self(output) + } +} diff --git a/src/lib/constants.rs b/src/lib/constants.rs new file mode 100644 index 0000000..27b639f --- /dev/null +++ b/src/lib/constants.rs @@ -0,0 +1,47 @@ +//! All constants. + +/// File name constants. +pub mod filename { + /// Cargo manifest file + pub const CARGO_TOML: &str = "Cargo.toml"; + /// Anchor manifest file + pub const ANCHOR_TOML: &str = "Anchor.toml"; + /// Starting point of a Rust library + pub const LIB_RS: &str = "lib.rs"; +} + +/// Directory name constants. +pub mod dirname { + /// `src` directory + pub const SRC: &str = "src"; + /// `target` directory + pub const TARGET: &str = "target"; + /// `deploy` directory under `target` folder + pub const DEPLOY: &str = "deploy"; + /// `programs_py` directory for Seahorse programs + pub const PROGRAMS_PY: &str = "programs_py"; +} + +/// File extension constants. +pub mod extension { + /// Rust extension + pub const RS: &str = "rs"; + /// TOML extension + pub const TOML: &str = "toml"; + /// ELF(.so) extension + pub const SO: &str = "so"; + /// JSON extension + pub const JSON: &str = "json"; + /// Python extension + pub const PY: &str = "py"; +} + +/// Emoji constants. +pub mod emoji { + use console::Emoji; + + /// Checkmark emoji + pub const CHECKMARK: Emoji = Emoji("✔", "+"); + /// Cross emoji + pub const CROSS: Emoji = Emoji("✖", "X"); +} diff --git a/src/lib/error.rs b/src/lib/error.rs new file mode 100644 index 0000000..e36233c --- /dev/null +++ b/src/lib/error.rs @@ -0,0 +1,22 @@ +//! Custom watch errors. + +use std::path::PathBuf; + +use miette::Diagnostic; +use thiserror::Error; + +/// Custom error definition for the crate. +#[derive(Error, Diagnostic, Debug)] +pub enum WatchError { + /// This error occurs when the program runs in a directory that's doesn't contain a Solana program. + #[error("Invalid program directory: `{0}`")] + InvalidProgramDirectory(PathBuf), + + /// Command is not installed in user's machine. + #[error("Command not found: `{0}`")] + CommandNotFound(&'static str), + + /// This most likely happens when the keypair file is not in a valid form. + #[error("Could not get keypair file: `{0}`")] + CouldNotGetKeypair(String), +} diff --git a/src/lib/framework.rs b/src/lib/framework.rs new file mode 100644 index 0000000..b016e55 --- /dev/null +++ b/src/lib/framework.rs @@ -0,0 +1,206 @@ +//! Framework traits. + +use std::{ + collections::HashSet, + path::{Path, PathBuf}, + sync::Arc, +}; + +use async_trait::async_trait; +use miette::IntoDiagnostic; +use tokio::fs; +use watchexec::filter::Filterer; + +use crate::{ + action::WAction, + command::WCommand, + constants::{dirname, extension}, + framework_utils::{ + create_globset_filterer, find_and_update_program_id, get_program_path, get_watch_pathset, + start_test_validator, + }, + progress::Progress, +}; + +/// Watchable Solana program framework. +/// +/// This trait is a supertrait of [`Framework`]. +#[async_trait] +pub trait WatchableFramework: Framework + Send + Sync { + /// Paths to watch. + /// + /// Watchexec puts OS-level watchers on all files under the given paths and it filters them + /// later with the specified [`WatchableFramework::filterer`] afterwards. This means it's not + /// a good idea to watch directories with great number of files inside it. + /// See [watchexec#241](https://github.com/watchexec/watchexec/issues/241) for more information. + /// + /// Default implementation is for Rust. + async fn pathset(&self) -> miette::Result> { + get_watch_pathset(self.origin()).await + } + + /// Filterer implementation that filters events. + /// + /// Default implementation is for Rust. + async fn filterer(&self) -> Arc { + let filters = []; + let ignores = []; + let extensions = [ + extension::RS, + extension::TOML, + extension::SO, + extension::JSON, + ]; + + create_globset_filterer(self.origin(), &filters, &ignores, &extensions).await + } + + /// Callback to run when an event has occured and it passed the [`Filterer`]. + /// + /// Default implementation is for Rust. + async fn on_action(&self, action: WAction) -> miette::Result<()> { + // Saving unique program paths because multiple files can be modified within the same + // action. This way, we don't rebuild the same program in the same action. + let mut unique_program_paths = HashSet::new(); + for action_path in action.get_unique_paths() { + if let Some(ext) = action_path.extension().and_then(|ext| ext.to_str()) { + match ext { + extension::RS | extension::TOML => { + let program_path = get_program_path(action_path).await?; + unique_program_paths.insert(program_path); + } + extension::SO => { + self.deploy(action_path).await.spawn().await?; + } + extension::JSON => { + self.update_program_id(action_path).await?; + } + _ => (), + } + } + } + + for program_path in unique_program_paths { + self.build(&program_path).await.spawn().await?; + } + + Ok(()) + } +} + +/// Solana program framework. +#[async_trait] +pub trait Framework: Send + Sync { + /// Origin is the root directory of the project and other paths will be derived from this path. + fn origin(&self) -> &Path; + + /// Handle the necessary checks and initialize the framework. + /// + /// This is called before watching starts. + async fn initialize(&self) -> miette::Result<()> { + self.check_toolset().await?; + self.map_program_names().await?; + + Progress::new() + .message("Starting Solana test validator...") + .success_message("Running Solana test validator") + .error_message("Could not start Solana test validator") + .spinner_with(|| async { start_test_validator(self.origin()).await }) + .await?; + + // If `target/deploy` doesn't exist, build the programs first to create the program keypair + // and program ELF + let deploy_path = self.origin().join(dirname::TARGET).join(dirname::DEPLOY); + if !deploy_path.exists() { + Progress::new() + .message("Setting up...") + .success_message("Setup success") + .error_message("Setup error") + .spinner_with(|| async { self.build(self.origin()).await.output().await }) + .await?; + } + + let mut deploy_dir = fs::read_dir(deploy_path).await.into_diagnostic()?; + let mut keypair_paths = vec![]; + let mut elf_paths = vec![]; + while let Some(entry) = deploy_dir.next_entry().await.into_diagnostic()? { + if let Some(Some(ext)) = entry.path().extension().map(|ext| ext.to_str()) { + match ext { + extension::JSON => keypair_paths.push(entry.path()), + extension::SO => elf_paths.push(entry.path()), + _ => (), + } + } + } + + // Get unique build paths + let mut unique_build_paths = HashSet::new(); + for paths in [&keypair_paths, &elf_paths] { + for path in paths { + if let Some(program_path) = self.get_program_path(path).await { + unique_build_paths.insert(program_path); + } + } + } + + Progress::new() + .message("Checking program ids...") + .success_message("Program ids are up to date") + .error_message("Couldn't update program ids") + .progress_with(keypair_paths, |keypair_path| async move { + self.update_program_id(&keypair_path).await + }) + .await?; + + Progress::new() + .message("Building...") + .success_message("Built programs") + .error_message("Couldn't build programs") + .progress_with(unique_build_paths, |build_path| async move { + self.build(&build_path).await.output().await + }) + .await?; + + Progress::new() + .message("Deploying programs...") + .success_message("Deployed programs") + .error_message("Couldn't deploy programs") + .progress_with(elf_paths, |elf_path| async move { + self.deploy(&elf_path).await.output().await + }) + .await?; + + println!(); + + Ok(()) + } + + /// Check the installed toolsets, e.g Solana CLI. + async fn check_toolset(&self) -> miette::Result<()>; + + /// Read and cache the program names with their paths to not use filesystem on every action. + async fn map_program_names(&self) -> miette::Result<()>; + + /// Get the program's root directory path based on the given path. + /// + /// The given path can be any path that allows a way to find the program's path, e.g program's + /// keypair file is named after the program's name and it can be used to get the program's path. + async fn get_program_path(&self, path: &Path) -> Option; + + /// Update the program id. + /// + /// Default implementation is for Rust. + async fn update_program_id(&self, program_keypair_path: &Path) -> miette::Result<()> { + if let Some(program_path) = self.get_program_path(program_keypair_path).await { + find_and_update_program_id(program_path, program_keypair_path).await?; + } + + Ok(()) + } + + /// Build command to run. + async fn build(&self, program_path: &Path) -> WCommand; + + /// Deploy command to run. + async fn deploy(&self, elf_path: &Path) -> WCommand; +} diff --git a/src/lib/framework_utils.rs b/src/lib/framework_utils.rs new file mode 100644 index 0000000..055af64 --- /dev/null +++ b/src/lib/framework_utils.rs @@ -0,0 +1,379 @@ +//! Utilities for framework implementations. + +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::Arc, +}; + +use lazy_static::lazy_static; +use miette::IntoDiagnostic; +use regex::{Match, Regex, RegexBuilder}; +use tokio::{fs, sync::RwLock, time}; +use watchexec_filterer_globset::GlobsetFilterer; + +use crate::{ + command::WCommand, + constants::{dirname, extension, filename}, + error::WatchError, + glob::glob, + toml::read_cargo_toml, +}; + +/// A mapping of program names and their paths. Using `RwLock` because the process is read heavy. +#[derive(Default)] +pub struct ProjectMap(Arc>>); + +impl ProjectMap { + /// Get the program's path from the given path. Mainly used for getting the program path from + /// program keypair or ELF path. + pub async fn get_program_path>(&self, path: P) -> Option { + let program_name = match path.as_ref().extension().map(|ext| ext.to_str()) { + Some(Some(ext)) => match ext { + extension::JSON => ProgramName::from_keypair_path(path), + extension::SO => ProgramName::from_elf_path(path), + _ => None, + }, + _ => None, + }; + + match program_name { + Some(program_name) => match self + .get_program_path_from_name(program_name.original()) + .await + { + Some(program_path) => Some(program_path), + None => { + self.get_program_path_from_name(program_name.kebab_case()) + .await + } + }, + None => None, + } + } + + /// Set the program path based on program name. + pub async fn set_program_path(&self, name: S, path: P) + where + S: Into, + P: Into, + { + let mut program_hm = self.0.write().await; + program_hm.insert(name.into(), path.into()); + } + + /// Get the program path from the program name. + async fn get_program_path_from_name>(&self, name: S) -> Option { + self.0 + .read() + .await + .get(name.as_ref()) + .map(|path| path.to_owned()) + } +} + +/// Utility struct to get the program name. +/// +/// Solana build tools generate the keypair name as `-keypair.json` and ELF name as +/// `.so`. Since `` is always in snake case, we are not able to get the +/// program name. That's because the programs named "hello-world" and "hello_world" will have the +/// exact same output files. +#[derive(Debug)] +pub struct ProgramName(String); + +impl ProgramName { + /// Create a new [`ProgramName`] from the given `name`. + pub fn new>(name: S) -> Self { + Self(name.into()) + } + + /// Get the program's name from the program keypair path. + /// + /// This function utilizes the fact that the program keypair names are in the format of + /// `-keypair.json`. + pub fn from_keypair_path>(program_keypair_path: P) -> Option { + Self::from_path(program_keypair_path, "-keypair.json") + } + + /// Get the program's name from the program ELF path. + /// + /// This function utilizes the fact that the program ELF names are in the format of + /// `.so`. + pub fn from_elf_path>(program_elf_path: P) -> Option { + Self::from_path(program_elf_path, ".so") + } + + /// Get the program's name by getting the file name from the `path` and stripping the `suffix`. + fn from_path(path: P, suffix: S) -> Option + where + P: AsRef, + S: AsRef, + { + path.as_ref() + .file_name() + .and_then(|name| name.to_str()) + .filter(|name| name.ends_with(suffix.as_ref())) + .map(|name| Self::new(name.trim_end_matches(suffix.as_ref()))) + } + + /// Reference to the original program name. + pub fn original(&self) -> &str { + &self.0 + } + + /// Convert the original program name to kebab-case. + pub fn kebab_case(&self) -> String { + self.0.replace('_', "-") + } +} + +/// Start a new test validator by running `solana-test-validator` command. +/// +/// This won't have any effect if there is already a running test validator. +/// +/// NOTE: This function will spawn a tokio task because `solana-test-validator` command never +/// resolves. It will then sleep for a small duration to give time for the initialization. This +/// means it will not confirm that the test validator has started. +pub async fn start_test_validator>(origin: P) -> miette::Result<()> { + let origin = origin.into(); + tokio::spawn(async { + let _ = WCommand::new("solana-test-validator") + .current_dir(origin) + .output() + .await; + }); + + // Wait 2 seconds for the test validator to start + time::sleep(time::Duration::from_secs(2)).await; + + Ok(()) +} + +/// Get all the directory paths that will be watched by default. +/// +/// If the `origin` is a workspace, the paths will be filtered by `workspace.members` and +/// `workspace.exclude`. Otherwise it's the `src` dir by default. +/// +/// Paths always include `target/deploy`. +pub async fn get_watch_pathset>(origin: P) -> miette::Result> { + let mut paths = vec![Path::new(dirname::TARGET).join(dirname::DEPLOY)]; + match filter_workspace_programs(origin).await? { + Some(filtered_paths) => paths.extend(filtered_paths), + None => paths.push(PathBuf::from(dirname::SRC)), + } + + Ok(paths) +} + +/// Filter workspace programs based on the manifest file at `origin`. +/// +/// Returns `Ok(None)` if the `origin` is not a workspace but has a manifest file. +async fn filter_workspace_programs>( + origin: P, +) -> miette::Result>> { + let manifest = read_cargo_toml(&origin).await?; + match manifest.workspace { + Some(workspace) => { + let paths = glob(origin.as_ref(), workspace.members, workspace.exclude, true).await?; + Ok(Some(paths)) + } + None => Ok(None), + } +} + +/// Get a mapping of program names and paths based on the manifest file at `origin`. +pub async fn get_program_name_path_hashmap>( + origin: P, +) -> miette::Result> { + let mut program_name_path_hm = HashMap::new(); + let program_paths = filter_workspace_programs(&origin) + .await? + .unwrap_or(vec![origin.as_ref().to_path_buf()]); + for program_path in program_paths { + if let Ok(manifest) = read_cargo_toml(&program_path).await { + if let Some(package) = manifest.package { + program_name_path_hm.insert(package.name, program_path); + } + } + } + + Ok(program_name_path_hm) +} + +/// Get program's root path by running `cargo locate-project` command. +pub async fn get_program_path>(modified_file_path: P) -> miette::Result { + let output = WCommand::new("cargo locate-project --message-format plain") + .current_dir(modified_file_path.as_ref().parent().unwrap()) + .output() + .await?; + if output.status().success() { + Ok(Path::new(output.stdout().trim_end_matches('\n')) + .parent() + .unwrap() + .to_path_buf()) + } else { + Err(WatchError::CommandNotFound("cargo locate-project"))? + } +} + +/// Get the keypair's address by running `solana address` command. +pub async fn get_pubkey_from_keypair_path>( + keypair_path: P, +) -> miette::Result { + let keypair_output = WCommand::new(format!( + "solana address -k {}", + keypair_path.as_ref().display() + )) + .output() + .await?; + + if !keypair_output.status().success() { + return Err(WatchError::CouldNotGetKeypair( + keypair_output.stderr().into(), + ))?; + } + + let program_id = keypair_output.stdout().trim_end_matches('\n'); + + Ok(program_id.to_owned()) +} + +/// Find the file that includes `declare_id!` macro and update the program id if it has changed. +/// +/// This function will check `lib.rs` first and **only** if it doesn't find the declaration it will +/// then check all the remaining source files. +pub async fn find_and_update_program_id( + program_path: P1, + program_keypair_path: P2, +) -> miette::Result<()> +where + P1: AsRef, + P2: AsRef, +{ + // Get the keypair program id + let program_id = get_pubkey_from_keypair_path(program_keypair_path).await?; + + // Check lib.rs first for the program id + let src_path = program_path.as_ref().join(dirname::SRC); + let lib_rs_path = src_path.join(filename::LIB_RS); + + if update_rust_program_id(lib_rs_path, &program_id).await? { + return Ok(()); + } + + // Check all the other files if the program_id doesn't exist in lib.rs + let rust_src_paths = glob(src_path, [format!("*.{}", extension::RS)], [], false).await?; + for path in rust_src_paths { + if update_rust_program_id(path, &program_id).await? { + // Not necessary to continue the loop after program id update + break; + } + } + + Ok(()) +} + +/// Update the file at the given path's `declare_id!` macro with the given program id. +/// +/// Returns whether the program id was updated successfully. +async fn update_rust_program_id(path: P, program_id: S) -> miette::Result +where + P: AsRef, + S: AsRef, +{ + lazy_static! { + static ref REGEX: Regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#) + .multi_line(true) + .build() + .unwrap(); + }; + + update_file_program_id_with(path, &program_id, |content| { + REGEX.captures(content).and_then(|captures| captures.get(3)) + }) + .await +} + +/// Update the file's `declare_id!` macro with the program id based on the given callback. +/// +/// Returns whether the program id was updated successfully. +pub async fn update_file_program_id_with( + path: P, + program_id: S, + cb: F, +) -> miette::Result +where + P: AsRef, + S: AsRef, + F: Fn(&str) -> Option>, +{ + let mut content = fs::read_to_string(&path).await.into_diagnostic()?; + if let Some(program_id_match) = + cb(&content).filter(|program_id_match| program_id_match.as_str() != program_id.as_ref()) + { + // Update the program id + content.replace_range(program_id_match.range(), program_id.as_ref()); + + // Save the file + fs::write(&path, content).await.into_diagnostic()?; + + return Ok(true); + } + + Ok(false) +} + +/// Get Solana build tool. +/// +/// Checks for `cargo build-sbf` and `cargo build-bpf` in order. +/// +/// Returns an error if the Solana build tools are not installed. +pub async fn get_bpf_or_sbf() -> miette::Result<&'static str> { + const BUILD_SBF: &str = "cargo build-sbf"; + const BUILD_BPF: &str = "cargo build-bpf"; + + let build_cmd = if WCommand::exists(BUILD_SBF).await { + BUILD_SBF + } else if WCommand::exists(BUILD_BPF).await { + BUILD_BPF + } else { + return Err(WatchError::CommandNotFound("solana"))?; + }; + + Ok(build_cmd) +} + +/// Create a globset filterer that will be used to filter the watched files. +/// +/// The filterer will always ignore `target`, `test-ledger` and `node_modules` paths. +pub async fn create_globset_filterer>( + origin: P, + filters: &[&str], + ignores: &[&str], + extensions: &[&str], +) -> Arc { + let filters = filters + .iter() + .map(|glob| (glob.to_string(), None)) + .collect::)>>(); + let ignores = [ + &[ + "**/*/target/**/*", + "**/*/test-ledger/**/*", + "**/*/node_modules/**/*", + ], + ignores, + ] + .concat() + .iter() + .map(|glob| (glob.to_string(), None)) + .collect::)>>(); + let ignore_files = []; + let extensions = extensions.iter().map(|ext| ext.into()); + + Arc::new( + GlobsetFilterer::new(origin, filters, ignores, ignore_files, extensions) + .await + .unwrap(), + ) +} diff --git a/src/lib/glob.rs b/src/lib/glob.rs new file mode 100644 index 0000000..aac358f --- /dev/null +++ b/src/lib/glob.rs @@ -0,0 +1,82 @@ +//! Custom `glob` implementation. + +use std::path::{Path, PathBuf}; + +use async_recursion::async_recursion; +use globset::{GlobBuilder, GlobSet, GlobSetBuilder}; +use miette::IntoDiagnostic; +use tokio::fs::{self, DirEntry}; + +/// Custom `glob` implementation to filter through pathnames in a directory. +/// +/// Returns all the matching paths based on the given `path` and included/excluded globs. +pub async fn glob( + path: P, + include_globs: I, + exclude_globs: E, + literal_seperator: bool, +) -> miette::Result> +where + P: AsRef + Send + Sync, + I: IntoIterator, + E: IntoIterator, +{ + let include_globset = create_globset(include_globs, literal_seperator)?; + let exclude_globset = create_globset(exclude_globs, literal_seperator)?; + + let mut matches = vec![]; + recursively_read_dir_mut(&path, &mut |entry| { + let is_match = entry + .path() + .strip_prefix(&path) + .ok() + .and_then(|relative_path| relative_path.to_str()) + .map(|s| include_globset.is_match(s) && !exclude_globset.is_match(s)) + .unwrap_or(false); + + if is_match { + matches.push(entry.path()); + } + }) + .await; + + Ok(matches) +} + +/// Create a [`GlobSet`] from the given `globs`. +fn create_globset>( + globs: G, + literal_seperator: bool, +) -> miette::Result { + let mut globset_builder = GlobSetBuilder::new(); + for glob in globs { + globset_builder.add( + GlobBuilder::new(&glob) + .literal_separator(literal_seperator) + .build() + .into_diagnostic()?, + ); + } + + globset_builder.build().into_diagnostic() +} + +/// Recursively read the given directory with mutable borrowed callback on each entry. +#[async_recursion] +async fn recursively_read_dir_mut(path: &P, cb: &mut F) +where + P: AsRef + Send + Sync, + F: FnMut(DirEntry) + Send + Sync, +{ + if let Ok(mut read_dir) = fs::read_dir(path).await { + while let Ok(Some(entry)) = read_dir.next_entry().await { + if let Ok(metadata) = entry.metadata().await { + if metadata.is_dir() { + recursively_read_dir_mut(&path.as_ref().join(entry.file_name()), cb).await; + } + + cb(entry); + } + } + } +} diff --git a/src/lib/lib.rs b/src/lib/lib.rs new file mode 100644 index 0000000..46e3586 --- /dev/null +++ b/src/lib/lib.rs @@ -0,0 +1,22 @@ +//! Watch [Solana](https://solana.com) programs. +//! +//! # Binary +//! The binary implements hot reloading for popular Solana frameworks. +//! +//! Check out the [repository](https://github.com/acheroncrypto/watchso) +//! for installation and usage of the binary. + +#![warn(missing_docs)] + +pub mod action; +pub mod command; +pub mod constants; +pub mod error; +pub mod framework; +pub mod framework_utils; +pub mod glob; +pub mod progress; +pub mod toml; + +mod watch; +pub use watch::watch; diff --git a/src/lib/progress.rs b/src/lib/progress.rs new file mode 100644 index 0000000..ccbb668 --- /dev/null +++ b/src/lib/progress.rs @@ -0,0 +1,129 @@ +//! Progress bars and spinners with consistent behaviour and styles. + +use std::future::Future; + +use console::Emoji; +use indicatif::{ProgressBar, ProgressStyle}; +use tokio::time::Duration; + +use crate::constants::emoji; + +/// Terminal progress utility struct. +#[derive(Default)] +pub struct Progress<'a> { + message: Option<&'a str>, + success_message: Option<&'a str>, + error_message: Option<&'a str>, + clear: bool, +} + +impl<'a> Progress<'a> { + /// Create a new [`Progress`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the message that will be displayed while the progress is ongoing. + pub fn message(&mut self, message: &'a str) -> &mut Self { + self.message = Some(message); + self + } + + /// Set the success message for the progress. + pub fn success_message(&mut self, message: &'a str) -> &mut Self { + self.success_message = Some(message); + self + } + + /// Set the error message for the progress. + pub fn error_message(&mut self, message: &'a str) -> &mut Self { + self.error_message = Some(message); + self + } + + /// Set whether the line should be cleared after the progress is finished. + #[allow(dead_code)] + pub fn clear(&mut self, clear: bool) -> &mut Self { + self.clear = clear; + self + } + + /// Spawn a spinner with the given callback. + pub async fn spinner_with(&self, cb: F) -> miette::Result + where + F: Fn() -> R, + R: Future>, + { + let pb = ProgressBar::new_spinner(); + pb.set_style(ProgressStyle::with_template(" {spinner:.green} {msg}").unwrap()); + pb.enable_steady_tick(Duration::from_millis(120)); + + if let Some(message) = self.message { + pb.set_message(message.to_owned()); + } + + let output = cb().await; + + match output { + Ok(_) => handle_output(&pb, self.success_message, "green", emoji::CHECKMARK), + Err(_) => handle_output(&pb, self.error_message, "red", emoji::CROSS), + } + + if self.clear { + pb.finish_and_clear() + } else { + pb.finish() + } + + output + } + + /// Spawn a progress bar with the given iterator and run the callback per element. + pub async fn progress_with(&self, iter: I, cb: F) -> miette::Result<()> + where + I: IntoIterator + Clone, + F: Fn(T) -> R, + R: Future>, + { + let vec = iter.into_iter().collect::>(); + let len = vec.len(); + let width = len.to_string().len(); + let pb = ProgressBar::new(len as u64); + pb.set_style( + ProgressStyle::with_template(&format!( + "[{{pos:>{width}}}/{{len:{width}}}] {{bar:.blue/white}} {{msg}}" + )) + .unwrap(), + ); + + if let Some(message) = self.message { + pb.set_message(message.to_owned()); + } + + for item in vec { + cb(item).await?; + pb.inc(1); + } + + handle_output(&pb, self.success_message, "green", emoji::CHECKMARK); + + if self.clear { + pb.finish_and_clear() + } else { + pb.finish() + } + + Ok(()) + } +} + +/// Show the output message with custom color and emoji prefix after progress has finished. +fn handle_output(pb: &ProgressBar, msg: Option<&str>, color: &str, prefix: Emoji) { + pb.set_style( + ProgressStyle::with_template(&format!("{{prefix:.{color}}} {{msg:.{color}}}")).unwrap(), + ); + pb.set_prefix(format!("{}", prefix)); + if let Some(msg) = msg { + pb.set_message(msg.to_owned()); + } +} diff --git a/src/lib/toml.rs b/src/lib/toml.rs new file mode 100644 index 0000000..c6d17a1 --- /dev/null +++ b/src/lib/toml.rs @@ -0,0 +1,19 @@ +//! TOML related methods. + +use std::path::Path; + +use cargo_toml::Manifest; +use miette::IntoDiagnostic; +use tokio::fs; + +use crate::constants::filename; + +/// Reads and parses the `Cargo.toml` at the given project directory. +pub async fn read_cargo_toml>(origin: P) -> miette::Result { + toml::from_str::( + &fs::read_to_string(origin.as_ref().join(filename::CARGO_TOML)) + .await + .into_diagnostic()?, + ) + .into_diagnostic() +} diff --git a/src/lib/watch.rs b/src/lib/watch.rs new file mode 100644 index 0000000..6b59872 --- /dev/null +++ b/src/lib/watch.rs @@ -0,0 +1,57 @@ +//! Custom watch implementation with [`watchexec`]. + +use std::{sync::Arc, time::Duration}; + +use console::style; +use miette::IntoDiagnostic; +use watchexec::{ + action::{Action, Outcome}, + config::{InitConfig, RuntimeConfig}, + Watchexec, +}; + +use crate::{action::WAction, error::WatchError, framework::WatchableFramework}; + +/// Watch the changes based on the specific [`WatchableFramework`] implementation. +pub async fn watch(framework: Arc) -> miette::Result<()> { + framework.initialize().await?; + + let mut runtime = RuntimeConfig::default(); + + runtime + .pathset(framework.pathset().await?) + .filterer(framework.filterer().await) + .action_throttle(Duration::from_millis(200)) + .on_action(move |action| { + let framework = framework.clone(); + async move { on_action(action, framework).await } + }); + + let init = InitConfig::default(); + + let watchexec = Watchexec::new(init, runtime)?; + watchexec.main().await.into_diagnostic()??; + + Ok(()) +} + +/// Top level action handler. +async fn on_action( + action: Action, + framework: Arc, +) -> Result<(), WatchError> { + let action = WAction::new(action); + + if action.is_interrupt() || action.is_terminate() { + action + .take() + .outcome(Outcome::both(Outcome::Stop, Outcome::Exit)); + return Ok(()); + } + + if let Err(err) = framework.on_action(action).await { + eprintln!("{} {}", style("[ERR]").red().bold(), err); + } + + Ok(()) +}