├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── .whitesource ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── benchy_bench.rs └── config.toml ├── changelog.md ├── doc ├── dep_graph.png ├── hm.gif ├── hm.webm └── subtree.png ├── rustfmt.toml └── src ├── config.toml ├── hm.rs └── lib ├── config.rs ├── hm_macro.rs ├── hmerror.rs └── mod.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rustfmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | 50 | clippy: 51 | name: Clippy 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | logs/ 3 | .#* 4 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW" 11 | } 12 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Changes are extremely welcome. I do not consider myself an amazing programmer and have no hangups about suggestions or complaints. 2 | 3 | If you have questions about how a change will be received, open an issue; if you have an already-baked idea, fork it and submit a PR. 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "anes" 22 | version = "0.1.6" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 25 | 26 | [[package]] 27 | name = "anstyle" 28 | version = "1.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 31 | 32 | [[package]] 33 | name = "autocfg" 34 | version = "1.1.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 37 | 38 | [[package]] 39 | name = "bitflags" 40 | version = "1.3.2" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 43 | 44 | [[package]] 45 | name = "bumpalo" 46 | version = "3.10.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" 49 | 50 | [[package]] 51 | name = "cast" 52 | version = "0.3.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 55 | 56 | [[package]] 57 | name = "cc" 58 | version = "1.0.73" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 61 | 62 | [[package]] 63 | name = "cfg-if" 64 | version = "1.0.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 67 | 68 | [[package]] 69 | name = "chrono" 70 | version = "0.4.38" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 73 | dependencies = [ 74 | "android-tzdata", 75 | "iana-time-zone", 76 | "js-sys", 77 | "num-traits", 78 | "wasm-bindgen", 79 | "windows-targets 0.52.0", 80 | ] 81 | 82 | [[package]] 83 | name = "ciborium" 84 | version = "0.2.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" 87 | dependencies = [ 88 | "ciborium-io", 89 | "ciborium-ll", 90 | "serde", 91 | ] 92 | 93 | [[package]] 94 | name = "ciborium-io" 95 | version = "0.2.1" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" 98 | 99 | [[package]] 100 | name = "ciborium-ll" 101 | version = "0.2.1" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" 104 | dependencies = [ 105 | "ciborium-io", 106 | "half", 107 | ] 108 | 109 | [[package]] 110 | name = "clap" 111 | version = "4.3.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" 114 | dependencies = [ 115 | "clap_builder", 116 | ] 117 | 118 | [[package]] 119 | name = "clap_builder" 120 | version = "4.3.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" 123 | dependencies = [ 124 | "anstyle", 125 | "bitflags", 126 | "clap_lex", 127 | ] 128 | 129 | [[package]] 130 | name = "clap_lex" 131 | version = "0.5.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" 134 | 135 | [[package]] 136 | name = "console" 137 | version = "0.15.8" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 140 | dependencies = [ 141 | "encode_unicode", 142 | "lazy_static", 143 | "libc", 144 | "unicode-width", 145 | "windows-sys 0.52.0", 146 | ] 147 | 148 | [[package]] 149 | name = "core-foundation-sys" 150 | version = "0.8.3" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 153 | 154 | [[package]] 155 | name = "criterion" 156 | version = "0.5.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" 159 | dependencies = [ 160 | "anes", 161 | "cast", 162 | "ciborium", 163 | "clap", 164 | "criterion-plot", 165 | "is-terminal", 166 | "itertools", 167 | "num-traits", 168 | "once_cell", 169 | "oorandom", 170 | "plotters", 171 | "rayon", 172 | "regex", 173 | "serde", 174 | "serde_derive", 175 | "serde_json", 176 | "tinytemplate", 177 | "walkdir", 178 | ] 179 | 180 | [[package]] 181 | name = "criterion-plot" 182 | version = "0.5.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 185 | dependencies = [ 186 | "cast", 187 | "itertools", 188 | ] 189 | 190 | [[package]] 191 | name = "crossbeam-channel" 192 | version = "0.5.5" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" 195 | dependencies = [ 196 | "cfg-if", 197 | "crossbeam-utils", 198 | ] 199 | 200 | [[package]] 201 | name = "crossbeam-deque" 202 | version = "0.8.1" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" 205 | dependencies = [ 206 | "cfg-if", 207 | "crossbeam-epoch", 208 | "crossbeam-utils", 209 | ] 210 | 211 | [[package]] 212 | name = "crossbeam-epoch" 213 | version = "0.9.9" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" 216 | dependencies = [ 217 | "autocfg", 218 | "cfg-if", 219 | "crossbeam-utils", 220 | "memoffset", 221 | "once_cell", 222 | "scopeguard", 223 | ] 224 | 225 | [[package]] 226 | name = "crossbeam-utils" 227 | version = "0.8.9" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" 230 | dependencies = [ 231 | "cfg-if", 232 | "once_cell", 233 | ] 234 | 235 | [[package]] 236 | name = "dirs" 237 | version = "4.0.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 240 | dependencies = [ 241 | "dirs-sys", 242 | ] 243 | 244 | [[package]] 245 | name = "dirs-next" 246 | version = "2.0.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 249 | dependencies = [ 250 | "cfg-if", 251 | "dirs-sys-next", 252 | ] 253 | 254 | [[package]] 255 | name = "dirs-sys" 256 | version = "0.3.7" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 259 | dependencies = [ 260 | "libc", 261 | "redox_users", 262 | "winapi", 263 | ] 264 | 265 | [[package]] 266 | name = "dirs-sys-next" 267 | version = "0.1.2" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 270 | dependencies = [ 271 | "libc", 272 | "redox_users", 273 | "winapi", 274 | ] 275 | 276 | [[package]] 277 | name = "either" 278 | version = "1.6.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 281 | 282 | [[package]] 283 | name = "encode_unicode" 284 | version = "0.3.6" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 287 | 288 | [[package]] 289 | name = "equivalent" 290 | version = "1.0.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" 293 | 294 | [[package]] 295 | name = "errno" 296 | version = "0.3.1" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 299 | dependencies = [ 300 | "errno-dragonfly", 301 | "libc", 302 | "windows-sys 0.48.0", 303 | ] 304 | 305 | [[package]] 306 | name = "errno-dragonfly" 307 | version = "0.1.2" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 310 | dependencies = [ 311 | "cc", 312 | "libc", 313 | ] 314 | 315 | [[package]] 316 | name = "getrandom" 317 | version = "0.2.7" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 320 | dependencies = [ 321 | "cfg-if", 322 | "libc", 323 | "wasi", 324 | ] 325 | 326 | [[package]] 327 | name = "half" 328 | version = "1.8.2" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" 331 | 332 | [[package]] 333 | name = "hashbrown" 334 | version = "0.14.0" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 337 | 338 | [[package]] 339 | name = "heck" 340 | version = "0.5.0" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 343 | 344 | [[package]] 345 | name = "hermit-abi" 346 | version = "0.1.19" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 349 | dependencies = [ 350 | "libc", 351 | ] 352 | 353 | [[package]] 354 | name = "hermit-abi" 355 | version = "0.3.1" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 358 | 359 | [[package]] 360 | name = "hm" 361 | version = "0.7.5" 362 | dependencies = [ 363 | "chrono", 364 | "console", 365 | "criterion", 366 | "dirs-next", 367 | "indicatif", 368 | "log", 369 | "serde", 370 | "shellexpand", 371 | "simplelog", 372 | "solvent", 373 | "strum", 374 | "strum_macros", 375 | "symlink", 376 | "sys-info", 377 | "toml", 378 | ] 379 | 380 | [[package]] 381 | name = "iana-time-zone" 382 | version = "0.1.47" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7" 385 | dependencies = [ 386 | "android_system_properties", 387 | "core-foundation-sys", 388 | "js-sys", 389 | "once_cell", 390 | "wasm-bindgen", 391 | "winapi", 392 | ] 393 | 394 | [[package]] 395 | name = "indexmap" 396 | version = "2.0.0" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 399 | dependencies = [ 400 | "equivalent", 401 | "hashbrown", 402 | ] 403 | 404 | [[package]] 405 | name = "indicatif" 406 | version = "0.16.2" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" 409 | dependencies = [ 410 | "console", 411 | "lazy_static", 412 | "number_prefix", 413 | "regex", 414 | ] 415 | 416 | [[package]] 417 | name = "io-lifetimes" 418 | version = "1.0.11" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 421 | dependencies = [ 422 | "hermit-abi 0.3.1", 423 | "libc", 424 | "windows-sys 0.48.0", 425 | ] 426 | 427 | [[package]] 428 | name = "is-terminal" 429 | version = "0.4.7" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 432 | dependencies = [ 433 | "hermit-abi 0.3.1", 434 | "io-lifetimes", 435 | "rustix", 436 | "windows-sys 0.48.0", 437 | ] 438 | 439 | [[package]] 440 | name = "itertools" 441 | version = "0.10.3" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 444 | dependencies = [ 445 | "either", 446 | ] 447 | 448 | [[package]] 449 | name = "itoa" 450 | version = "1.0.2" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 453 | 454 | [[package]] 455 | name = "js-sys" 456 | version = "0.3.59" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" 459 | dependencies = [ 460 | "wasm-bindgen", 461 | ] 462 | 463 | [[package]] 464 | name = "lazy_static" 465 | version = "1.4.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 468 | 469 | [[package]] 470 | name = "libc" 471 | version = "0.2.144" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" 474 | 475 | [[package]] 476 | name = "linux-raw-sys" 477 | version = "0.3.8" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 480 | 481 | [[package]] 482 | name = "log" 483 | version = "0.4.22" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 486 | 487 | [[package]] 488 | name = "memchr" 489 | version = "2.5.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 492 | 493 | [[package]] 494 | name = "memoffset" 495 | version = "0.6.5" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 498 | dependencies = [ 499 | "autocfg", 500 | ] 501 | 502 | [[package]] 503 | name = "num-traits" 504 | version = "0.2.15" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 507 | dependencies = [ 508 | "autocfg", 509 | ] 510 | 511 | [[package]] 512 | name = "num_cpus" 513 | version = "1.13.1" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 516 | dependencies = [ 517 | "hermit-abi 0.1.19", 518 | "libc", 519 | ] 520 | 521 | [[package]] 522 | name = "num_threads" 523 | version = "0.1.6" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 526 | dependencies = [ 527 | "libc", 528 | ] 529 | 530 | [[package]] 531 | name = "number_prefix" 532 | version = "0.4.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 535 | 536 | [[package]] 537 | name = "once_cell" 538 | version = "1.14.0" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" 541 | 542 | [[package]] 543 | name = "oorandom" 544 | version = "11.1.3" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" 547 | 548 | [[package]] 549 | name = "plotters" 550 | version = "0.3.1" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" 553 | dependencies = [ 554 | "num-traits", 555 | "plotters-backend", 556 | "plotters-svg", 557 | "wasm-bindgen", 558 | "web-sys", 559 | ] 560 | 561 | [[package]] 562 | name = "plotters-backend" 563 | version = "0.3.2" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" 566 | 567 | [[package]] 568 | name = "plotters-svg" 569 | version = "0.3.1" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" 572 | dependencies = [ 573 | "plotters-backend", 574 | ] 575 | 576 | [[package]] 577 | name = "proc-macro2" 578 | version = "1.0.78" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 581 | dependencies = [ 582 | "unicode-ident", 583 | ] 584 | 585 | [[package]] 586 | name = "quote" 587 | version = "1.0.35" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 590 | dependencies = [ 591 | "proc-macro2", 592 | ] 593 | 594 | [[package]] 595 | name = "rayon" 596 | version = "1.5.3" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" 599 | dependencies = [ 600 | "autocfg", 601 | "crossbeam-deque", 602 | "either", 603 | "rayon-core", 604 | ] 605 | 606 | [[package]] 607 | name = "rayon-core" 608 | version = "1.9.3" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" 611 | dependencies = [ 612 | "crossbeam-channel", 613 | "crossbeam-deque", 614 | "crossbeam-utils", 615 | "num_cpus", 616 | ] 617 | 618 | [[package]] 619 | name = "redox_syscall" 620 | version = "0.2.13" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 623 | dependencies = [ 624 | "bitflags", 625 | ] 626 | 627 | [[package]] 628 | name = "redox_users" 629 | version = "0.4.3" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 632 | dependencies = [ 633 | "getrandom", 634 | "redox_syscall", 635 | "thiserror", 636 | ] 637 | 638 | [[package]] 639 | name = "regex" 640 | version = "1.5.6" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 643 | dependencies = [ 644 | "regex-syntax", 645 | ] 646 | 647 | [[package]] 648 | name = "regex-syntax" 649 | version = "0.6.26" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 652 | 653 | [[package]] 654 | name = "rustix" 655 | version = "0.37.19" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" 658 | dependencies = [ 659 | "bitflags", 660 | "errno", 661 | "io-lifetimes", 662 | "libc", 663 | "linux-raw-sys", 664 | "windows-sys 0.48.0", 665 | ] 666 | 667 | [[package]] 668 | name = "rustversion" 669 | version = "1.0.7" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf" 672 | 673 | [[package]] 674 | name = "ryu" 675 | version = "1.0.10" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 678 | 679 | [[package]] 680 | name = "same-file" 681 | version = "1.0.6" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 684 | dependencies = [ 685 | "winapi-util", 686 | ] 687 | 688 | [[package]] 689 | name = "scopeguard" 690 | version = "1.1.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 693 | 694 | [[package]] 695 | name = "serde" 696 | version = "1.0.207" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" 699 | dependencies = [ 700 | "serde_derive", 701 | ] 702 | 703 | [[package]] 704 | name = "serde_derive" 705 | version = "1.0.207" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" 708 | dependencies = [ 709 | "proc-macro2", 710 | "quote", 711 | "syn 2.0.48", 712 | ] 713 | 714 | [[package]] 715 | name = "serde_json" 716 | version = "1.0.81" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" 719 | dependencies = [ 720 | "itoa", 721 | "ryu", 722 | "serde", 723 | ] 724 | 725 | [[package]] 726 | name = "serde_spanned" 727 | version = "0.6.7" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" 730 | dependencies = [ 731 | "serde", 732 | ] 733 | 734 | [[package]] 735 | name = "shellexpand" 736 | version = "3.1.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" 739 | dependencies = [ 740 | "dirs", 741 | ] 742 | 743 | [[package]] 744 | name = "simplelog" 745 | version = "0.12.2" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" 748 | dependencies = [ 749 | "log", 750 | "termcolor", 751 | "time", 752 | ] 753 | 754 | [[package]] 755 | name = "solvent" 756 | version = "0.8.3" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "14a50198e546f29eb0a4f977763c8277ec2184b801923c3be71eeaec05471f16" 759 | 760 | [[package]] 761 | name = "strum" 762 | version = "0.26.3" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 765 | 766 | [[package]] 767 | name = "strum_macros" 768 | version = "0.26.4" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 771 | dependencies = [ 772 | "heck", 773 | "proc-macro2", 774 | "quote", 775 | "rustversion", 776 | "syn 2.0.48", 777 | ] 778 | 779 | [[package]] 780 | name = "symlink" 781 | version = "0.1.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" 784 | 785 | [[package]] 786 | name = "syn" 787 | version = "1.0.105" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 790 | dependencies = [ 791 | "proc-macro2", 792 | "quote", 793 | "unicode-ident", 794 | ] 795 | 796 | [[package]] 797 | name = "syn" 798 | version = "2.0.48" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 801 | dependencies = [ 802 | "proc-macro2", 803 | "quote", 804 | "unicode-ident", 805 | ] 806 | 807 | [[package]] 808 | name = "sys-info" 809 | version = "0.9.1" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" 812 | dependencies = [ 813 | "cc", 814 | "libc", 815 | ] 816 | 817 | [[package]] 818 | name = "termcolor" 819 | version = "1.1.3" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 822 | dependencies = [ 823 | "winapi-util", 824 | ] 825 | 826 | [[package]] 827 | name = "thiserror" 828 | version = "1.0.31" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" 831 | dependencies = [ 832 | "thiserror-impl", 833 | ] 834 | 835 | [[package]] 836 | name = "thiserror-impl" 837 | version = "1.0.31" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" 840 | dependencies = [ 841 | "proc-macro2", 842 | "quote", 843 | "syn 1.0.105", 844 | ] 845 | 846 | [[package]] 847 | name = "time" 848 | version = "0.3.10" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "82501a4c1c0330d640a6e176a3d6a204f5ec5237aca029029d21864a902e27b0" 851 | dependencies = [ 852 | "itoa", 853 | "libc", 854 | "num_threads", 855 | "time-macros", 856 | ] 857 | 858 | [[package]] 859 | name = "time-macros" 860 | version = "0.2.4" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" 863 | 864 | [[package]] 865 | name = "tinytemplate" 866 | version = "1.2.1" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 869 | dependencies = [ 870 | "serde", 871 | "serde_json", 872 | ] 873 | 874 | [[package]] 875 | name = "toml" 876 | version = "0.8.19" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 879 | dependencies = [ 880 | "serde", 881 | "serde_spanned", 882 | "toml_datetime", 883 | "toml_edit", 884 | ] 885 | 886 | [[package]] 887 | name = "toml_datetime" 888 | version = "0.6.8" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 891 | dependencies = [ 892 | "serde", 893 | ] 894 | 895 | [[package]] 896 | name = "toml_edit" 897 | version = "0.22.20" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" 900 | dependencies = [ 901 | "indexmap", 902 | "serde", 903 | "serde_spanned", 904 | "toml_datetime", 905 | "winnow", 906 | ] 907 | 908 | [[package]] 909 | name = "unicode-ident" 910 | version = "1.0.1" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 913 | 914 | [[package]] 915 | name = "unicode-width" 916 | version = "0.1.9" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 919 | 920 | [[package]] 921 | name = "walkdir" 922 | version = "2.3.2" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 925 | dependencies = [ 926 | "same-file", 927 | "winapi", 928 | "winapi-util", 929 | ] 930 | 931 | [[package]] 932 | name = "wasi" 933 | version = "0.11.0+wasi-snapshot-preview1" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 936 | 937 | [[package]] 938 | name = "wasm-bindgen" 939 | version = "0.2.82" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" 942 | dependencies = [ 943 | "cfg-if", 944 | "wasm-bindgen-macro", 945 | ] 946 | 947 | [[package]] 948 | name = "wasm-bindgen-backend" 949 | version = "0.2.82" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" 952 | dependencies = [ 953 | "bumpalo", 954 | "log", 955 | "once_cell", 956 | "proc-macro2", 957 | "quote", 958 | "syn 1.0.105", 959 | "wasm-bindgen-shared", 960 | ] 961 | 962 | [[package]] 963 | name = "wasm-bindgen-macro" 964 | version = "0.2.82" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" 967 | dependencies = [ 968 | "quote", 969 | "wasm-bindgen-macro-support", 970 | ] 971 | 972 | [[package]] 973 | name = "wasm-bindgen-macro-support" 974 | version = "0.2.82" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" 977 | dependencies = [ 978 | "proc-macro2", 979 | "quote", 980 | "syn 1.0.105", 981 | "wasm-bindgen-backend", 982 | "wasm-bindgen-shared", 983 | ] 984 | 985 | [[package]] 986 | name = "wasm-bindgen-shared" 987 | version = "0.2.82" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" 990 | 991 | [[package]] 992 | name = "web-sys" 993 | version = "0.3.58" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" 996 | dependencies = [ 997 | "js-sys", 998 | "wasm-bindgen", 999 | ] 1000 | 1001 | [[package]] 1002 | name = "winapi" 1003 | version = "0.3.9" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1006 | dependencies = [ 1007 | "winapi-i686-pc-windows-gnu", 1008 | "winapi-x86_64-pc-windows-gnu", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "winapi-i686-pc-windows-gnu" 1013 | version = "0.4.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1016 | 1017 | [[package]] 1018 | name = "winapi-util" 1019 | version = "0.1.5" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1022 | dependencies = [ 1023 | "winapi", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "winapi-x86_64-pc-windows-gnu" 1028 | version = "0.4.0" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1031 | 1032 | [[package]] 1033 | name = "windows-sys" 1034 | version = "0.48.0" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1037 | dependencies = [ 1038 | "windows-targets 0.48.0", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "windows-sys" 1043 | version = "0.52.0" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1046 | dependencies = [ 1047 | "windows-targets 0.52.0", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "windows-targets" 1052 | version = "0.48.0" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 1055 | dependencies = [ 1056 | "windows_aarch64_gnullvm 0.48.0", 1057 | "windows_aarch64_msvc 0.48.0", 1058 | "windows_i686_gnu 0.48.0", 1059 | "windows_i686_msvc 0.48.0", 1060 | "windows_x86_64_gnu 0.48.0", 1061 | "windows_x86_64_gnullvm 0.48.0", 1062 | "windows_x86_64_msvc 0.48.0", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "windows-targets" 1067 | version = "0.52.0" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 1070 | dependencies = [ 1071 | "windows_aarch64_gnullvm 0.52.0", 1072 | "windows_aarch64_msvc 0.52.0", 1073 | "windows_i686_gnu 0.52.0", 1074 | "windows_i686_msvc 0.52.0", 1075 | "windows_x86_64_gnu 0.52.0", 1076 | "windows_x86_64_gnullvm 0.52.0", 1077 | "windows_x86_64_msvc 0.52.0", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "windows_aarch64_gnullvm" 1082 | version = "0.48.0" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 1085 | 1086 | [[package]] 1087 | name = "windows_aarch64_gnullvm" 1088 | version = "0.52.0" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 1091 | 1092 | [[package]] 1093 | name = "windows_aarch64_msvc" 1094 | version = "0.48.0" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1097 | 1098 | [[package]] 1099 | name = "windows_aarch64_msvc" 1100 | version = "0.52.0" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 1103 | 1104 | [[package]] 1105 | name = "windows_i686_gnu" 1106 | version = "0.48.0" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1109 | 1110 | [[package]] 1111 | name = "windows_i686_gnu" 1112 | version = "0.52.0" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 1115 | 1116 | [[package]] 1117 | name = "windows_i686_msvc" 1118 | version = "0.48.0" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1121 | 1122 | [[package]] 1123 | name = "windows_i686_msvc" 1124 | version = "0.52.0" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 1127 | 1128 | [[package]] 1129 | name = "windows_x86_64_gnu" 1130 | version = "0.48.0" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1133 | 1134 | [[package]] 1135 | name = "windows_x86_64_gnu" 1136 | version = "0.52.0" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 1139 | 1140 | [[package]] 1141 | name = "windows_x86_64_gnullvm" 1142 | version = "0.48.0" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1145 | 1146 | [[package]] 1147 | name = "windows_x86_64_gnullvm" 1148 | version = "0.52.0" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 1151 | 1152 | [[package]] 1153 | name = "windows_x86_64_msvc" 1154 | version = "0.48.0" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1157 | 1158 | [[package]] 1159 | name = "windows_x86_64_msvc" 1160 | version = "0.52.0" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 1163 | 1164 | [[package]] 1165 | name = "winnow" 1166 | version = "0.6.18" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" 1169 | dependencies = [ 1170 | "memchr", 1171 | ] 1172 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hm" 3 | version = "0.7.5" 4 | authors = ["Matt Weller "] 5 | edition = "2018" 6 | description = "homemaker. Slightly more than yet another dotfile manager." 7 | license = "Apache-2.0" 8 | repository = "https://github.com/hlmtre/homemaker" 9 | keywords = ["dotfiles", "management", "dotfile"] 10 | categories = ["command-line-utilities", "filesystem"] 11 | include = ["src/**/*", "LICENSE", "README.md", "changelog.md"] 12 | 13 | [lib] 14 | name = "hm" 15 | path = "src/lib/mod.rs" 16 | 17 | [[bin]] 18 | name = "hm" 19 | path = "src/hm.rs" 20 | 21 | [dev-dependencies] 22 | criterion = "0.5" 23 | 24 | [[bench]] 25 | name = "benchy_bench" 26 | path = "benches/benchy_bench.rs" 27 | harness = false 28 | 29 | [dependencies] 30 | serde = { version = "1.0", features = ["derive"] } 31 | toml = "0.8" 32 | dirs-next = "2.0" 33 | shellexpand = "3.1.0" 34 | symlink = "0.1.0" 35 | solvent = "0.8.3" 36 | indicatif = "0.16.2" 37 | console = "0.15.8" 38 | sys-info = "0.9.1" 39 | strum_macros = "0.26.4" 40 | strum = "0.26.3" 41 | log = "0.4.22" 42 | simplelog = "0.12" 43 | chrono = "0.4" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Matt Weller 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Continuous integration](https://github.com/hlmtre/homemaker/actions/workflows/rust.yml/badge.svg)](https://github.com/hlmtre/homemaker/actions/workflows/rust.yml) 2 | 3 | Have a dotfiles directory with all your stuff in it? Have homemaker put everything in its right place. 4 | 5 | Check out the [changelog](changelog.md) 6 | --------------------------------------- 7 | 8 | **homemaker in action** 9 | 10 | ![hm in action](doc/hm.gif) 11 | 12 | installation 13 | ============ 14 | * from crates.io: `cargo install hm` 15 | * from github (may be in some state of flux): `cargo install --git https://github.com/hlmtre/homemaker` 16 | * cloned locally: `cargo install --path .` 17 | 18 | 1. create a `config.toml` file either anywhere (and specify `-c` when you run `hm`) or in `~/.config/homemaker/`. 19 | 2. enter things to do in `config.toml`. 20 | 21 | example: 22 | ``` toml 23 | ## config.toml 24 | 25 | [[obj]] 26 | file = 'tmux.conf' # simple things - symlink or copy a file somewhere 27 | source = '~/dotfiles/.tmux.conf' 28 | destination = '~/.tmux.conf' 29 | method = 'symlink' 30 | 31 | [[obj]] 32 | task = 'zt' # more complicated - a task. 33 | solution = 'cd ~/dotfiles/zt && git pull' 34 | dependencies = ['maim, slop'] 35 | os = 'linux::debian' 36 | 37 | [[obj]] 38 | task = 'maim_dependencies' 39 | solution = 'sudo apt install -y libxfixes-dev libglm-dev libxrandr-dev libglew-dev libegl1-mesa-dev libxcomposite-dev' 40 | os = 'linux::debian' # only if the platform matches. 41 | # valid OS values we differentiate between are: 42 | # linux::fedora 43 | # linux::debian 44 | # linux::ubuntu 45 | # windows 46 | 47 | [[obj]] 48 | task = 'maim' 49 | source = '~/dotfiles/zt/maim' 50 | solution = 'cd ~/dotfiles/zt/maim; make clean; cmake -DCMAKE_INSTALL_PREFIX="/usr" ./ && make && sudo make install' 51 | method = 'execute' 52 | dependencies = ['maim_dependencies'] 53 | 54 | [[obj]] 55 | task = 'slop' 56 | source = '~/dotfiles/zt/slop' 57 | solution = 'cd ~/dotfiles/zt/slop; make clean; cmake -DCMAKE_INSTALL_PREFIX="/usr" ./ && make && sudo make install' 58 | method = 'execute' 59 | os = 'linux::debian' 60 | 61 | [[obj]] 62 | task = 'slop' 63 | source = '~/dotfiles/zt/slop' 64 | solution = 'cd ~/dotfiles/zt/slop; make clean; cmake -DCMAKE_INSTALL_PREFIX="/usr" ./ && make && sudo make install' 65 | method = 'execute' 66 | os = 'linux::debian' 67 | 68 | [[obj]] 69 | task = 'nvim' 70 | method = 'execute' 71 | solution = "test -x /usr/local/bin/nvim || (git clone https://github.com/neovim/nevim.git ~/src/ && cd ~/src/neovim && make CMAKE_BUILD_TYPE=RelWithDebInfo CMAKE_INSTALL_PREFIX=/usr/local/ && sudo make install)" # if nvim is not there and executable, run our solution. 72 | dependencies = ['vim-plug'] 73 | 74 | [[obj]] 75 | task = 'vim-plug' 76 | method = 'execute' 77 | solution = "sh -c 'curl -fLo ~/.local/share/nvim/site/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'" 78 | ``` 79 | 3. `hm -c /path/to/your/config.toml` 80 | 81 | * simple `file` entries either symlink or copy a file somewhere - usually a config file. 82 | * tasks are more complicated actions to perform - run scripts, download/compile software, etc. they can be restricted to specific platforms (differentiated values are specified above in the `maim_dependencies` task). 83 | 84 | why homemaker? 85 | ============== 86 | * compared to say, gnu stow, homemaker supports more than just creating a mirrored symlinked filesystem. 87 | * dependency resolution: 88 | * specify a set of tasks to complete, each with their own dependencies, and watch as it completes them in some 89 | order that satisfies each task's dependencies. 90 | * for example, in the sample `config.toml` (the one i use, actually), `maim` depends on having some graphics libraries installed. 91 | i created a task called `maim_dependencies`, and `hm` will complete `maim_dependencies` before attempting to complete `maim`. 92 | * `zt` has two dependencies: `maim` and `slop`. `hm` will complete the entire dependency tree below `zt` before atttempting `zt`. 93 | * `homemaker` complains if the dependency tree cannot be solved, and shows you a hopefully-handy explanation why. 94 | ![dep graph](doc/dep_graph.png) 95 | * allows for specifying portions of the config to be executed (target tasks). only wanna run one task? `-t ` 96 | 97 | ![subtree](doc/subtree.png) 98 | 99 | homemaker unknowingly clobbers an existing dotfile manager written in Go some time ago. Linked [here](https://github.com/FooSoft/homemaker). 100 | ============================================================================================================================================ 101 | 102 | [![built with spacemacs](https://cdn.rawgit.com/syl20bnr/spacemacs/442d025779da2f62fc86c2082703697714db6514/assets/spacemacs-badge.svg)](http://spacemacs.org) 103 | and neovim. 104 | 105 | Could not be made without [rust-analyzer](https://github.com/rust-analyzer/rust-analyzer). 106 | -------------------------------------------------------------------------------- /benches/benchy_bench.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use hm::{ 3 | config::{deserialize_file, Config, ManagedObject}, 4 | get_task_batches, 5 | }; 6 | use std::collections::HashMap; 7 | 8 | fn criterion_benchmark(c: &mut Criterion) { 9 | let a: Config = deserialize_file("./benches/config.toml").unwrap(); 10 | let nodes: HashMap = Config::as_managed_objects(a); 11 | c.bench_function("get_task_batches", |b| { 12 | b.iter(|| get_task_batches(nodes.clone(), None)) 13 | }); 14 | } 15 | 16 | criterion_group!(benches, criterion_benchmark); 17 | criterion_main!(benches); 18 | -------------------------------------------------------------------------------- /benches/config.toml: -------------------------------------------------------------------------------- 1 | [[obj]] 2 | file = 'tmux.conf' 3 | source = '~/dotfiles/.tmux.conf' 4 | destination = '~/.tmux.conf' 5 | method = 'symlink' 6 | 7 | 8 | [[obj]] 9 | task = 'maim_dependencies' 10 | solution = 'sudo apt install -y libxfixes-dev libglm-dev libxrandr-dev libglew-dev libegl1-mesa-dev libxcomposite-dev' 11 | # os should be of the format either `windows` or `linux::` 12 | os = "linux::fedora" 13 | 14 | [[obj]] 15 | task = 'windows only' 16 | solution = 'rustup update' 17 | os = 'windows' -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | TODO: 2 | ==== 3 | 4 | * add ability to restrict tasks based on hostname. should be pretty simple. 5 | 6 | version 0.7.4 7 | ============= 8 | 9 | * add function to get latest log file, to be passed to your editor (like so: `nvr (hm log)`) 10 | * updates to the arg parsing timing so it parses args before it does anything (no more writing log files for just having run `hm clean`) 11 | 12 | version 0.7.1 13 | ============= 14 | 15 | * clippy lint cleanup 16 | 17 | version 0.7.0 18 | ============= 19 | 20 | * not 0.6.8 because our commandline args have changed and we have some new args to lib functions. 21 | * now supports passing -t to specify a task (and its dependencies) to complete. 22 | - this runs ONLY that subtree. 23 | * now REQUIRES -c or --config to manually specify config location. 24 | -------------------------------------------------------------------------------- /doc/dep_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlmtre/homemaker/a317202cefeeba238f8d17c07b4871b960e69af2/doc/dep_graph.png -------------------------------------------------------------------------------- /doc/hm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlmtre/homemaker/a317202cefeeba238f8d17c07b4871b960e69af2/doc/hm.gif -------------------------------------------------------------------------------- /doc/hm.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlmtre/homemaker/a317202cefeeba238f8d17c07b4871b960e69af2/doc/hm.webm -------------------------------------------------------------------------------- /doc/subtree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlmtre/homemaker/a317202cefeeba238f8d17c07b4871b960e69af2/doc/subtree.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 -------------------------------------------------------------------------------- /src/config.toml: -------------------------------------------------------------------------------- 1 | # simple managed object 2 | # [[obj]] 3 | # task = 'zt' <-- like 'name' but for, uh, tasks 4 | # solution = 'cd ~/dotfiles/zt && git pull' <-- shell to execute 5 | # dependencies = ['maim', 'slop'] <-- dependencies - do them first. Valid values: any other tasks, need not be specified before this one. 6 | 7 | # complex managed object 8 | # [[obj]] <-- required `obj` header for each managed object 9 | # file = '.Xresources' <-- name 10 | # source = '~/dotfiles/.Xresources' <-- for symlink/copy, source file location 11 | # destination = '~/.Xresources' <-- for symlink/copy, destination 12 | # method = 'symlink' <-- symlink or copy?. Valid values: symlink, copy. 13 | # post = "xrdb ~/.Xresources" <-- a wee shell script to execute upon SUCCESSFUL symlink/copy 14 | # force = 'true' <-- overwrite if file already exists. Valid values: true, false; unspecified defaults to false 15 | 16 | 17 | [[obj]] 18 | file = 'alacritty' 19 | source = '~/dotfiles/.config/alacritty' 20 | destination = '~/.config/alacritty' 21 | method = 'symlink' 22 | 23 | [[obj]] 24 | file = 'kitty' 25 | source = '~/dotfiles/.config/kitty' 26 | destination = '~/.config/kitty' 27 | method = 'symlink' 28 | 29 | [[obj]] 30 | file = '.neomuttrc' 31 | source = '~/dotfiles/.neomuttrc' 32 | destination = '~/.neomuttrc' 33 | method = 'symlink' 34 | force = 'true' 35 | 36 | [[obj]] 37 | file = 'starship.toml' 38 | source = '~/dotfiles/.config/starship.toml' 39 | destination = '~/.config/starship.toml' 40 | method = 'symlink' 41 | 42 | [[obj]] 43 | file = 'waybar' 44 | source = '~/dotfiles/.config/waybar' 45 | destination = '~/.config/waybar' 46 | method = 'symlink' 47 | 48 | [[obj]] 49 | file = '.Xresources' 50 | source = '~/dotfiles/.Xresources' 51 | destination = '~/.Xresources' 52 | method = 'symlink' 53 | post = "xrdb ~/.Xresources" 54 | force = 'true' 55 | 56 | [[obj]] 57 | file = 'sway' 58 | source = '~/dotfiles/.config/sway' 59 | destination = '~/.config/sway' 60 | method = 'symlink' 61 | 62 | [[obj]] 63 | file = 'rofi' 64 | source = '~/dotfiles/.config/rofi' 65 | destination = '~/.config/rofi' 66 | method = 'symlink' 67 | 68 | [[obj]] 69 | file = 'tmux.conf' 70 | source = '~/dotfiles/.tmux.conf' 71 | destination = '~/.tmux.conf' 72 | method = 'symlink' 73 | post = 'git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm' 74 | 75 | [[obj]] 76 | file = 'gtk2' 77 | source = '~/dotfiles/.config/gtk-2.0' 78 | destination = '~/.config/gtk-2.0' 79 | method = 'symlink' 80 | force = 'true' 81 | 82 | [[obj]] 83 | file = 'gtk3' 84 | source = '~/dotfiles/.config/gtk-3.0' 85 | destination = '~/.config/gtk-3.0' 86 | method = 'symlink' 87 | force = 'true' 88 | 89 | [[obj]] 90 | file = 'nvim_config' 91 | source = '~/dotfiles/.config/nvim' 92 | destination = '~/.config/nvim' 93 | method = 'symlink' 94 | force = 'true' 95 | 96 | [[obj]] 97 | task = 'nvim' 98 | method = 'execute' 99 | solution = "test -x /usr/local/bin/nvim || (git clone https://github.com/neovim/neovim.git ~/src/neovim && cd ~/src/neovim && make CMAKE_BUILD_TYPE=RelWithDebInfo CMAKE_INSTALL_PREFIX=/usr/local/ && sudo make install)" 100 | dependencies = ['packer', 'nvim_deps', 'ninja-cmake'] 101 | 102 | [[obj]] 103 | task = 'nvim_deps' 104 | method = 'execute' 105 | solution = 'sudo apt install -y ninja-build gettext libtool libtool-bin autoconf automake cmake g++ pkg-config unzip curl gettext' 106 | os = 'linux::debian' 107 | 108 | [[obj]] 109 | task = 'nvim_deps' 110 | method = 'execute' 111 | solution = 'sudo yum -y install ninja-build libtool autoconf automake cmake gcc gcc-c++ make pkgconfig unzip patch gettext curl' 112 | os = 'linux::fedora' 113 | 114 | [[obj]] 115 | task = 'ninja-cmake' 116 | method = 'execute' 117 | solution = 'pip3 install --user ninja cmake' 118 | 119 | [[obj]] 120 | task = 'packer' 121 | method = 'execute' 122 | solution = "git clone --depth 1 https://github.com/wbthomason/packer.nvim ~/.local/share/nvim/site/pack/packer/start/packer.nvim" 123 | 124 | [[obj]] 125 | file = 'fish' 126 | source = '~/dotfiles/.config/fish' 127 | destination = '~/.config/fish' 128 | method = 'symlink' 129 | force = 'true' 130 | 131 | [[obj]] 132 | task = 'zt-fedora-wayland' 133 | solution = 'cd ~/dotfiles/zt && git pull' 134 | dependencies = ['grim', 'slurp'] 135 | os = 'linux::fedora' 136 | 137 | [[obj]] 138 | task = 'grim' 139 | solution = 'test -d ~/src/grim/ || (git clone https://github.com/emersion/grim.git ~/src/grim && cd ~/src/grim && meson build && sudo ninja -C build install)' 140 | method = 'execute' 141 | os = 'linux::fedora' 142 | 143 | [[obj]] 144 | task = 'slurp' 145 | method = 'execute' 146 | solution = 'test -d ~/src/slurp/ || (git clone https://github.com/emersion/slurp.git ~/src/slurp && cd ~/src/slurp && meson build && sudo ninja -C build install)' 147 | os = 'linux::fedora' 148 | 149 | [[obj]] 150 | task = 'wofi' 151 | method = 'execute' 152 | solution = 'test -d ~/src/wofi/ || (hg clone https://hg.sr.ht/~scoopta/wofi ~/src/wofi && cd ~/src/wofi && meson build && sudo ninja -C build install)' 153 | os = 'linux::fedora' 154 | 155 | [[obj]] 156 | task = 'zt' 157 | solution = 'cd ~/dotfiles/zt && git pull' 158 | dependencies = ['maim', 'slop'] 159 | os = 'linux::debian' 160 | 161 | [[obj]] 162 | task = 'slop' 163 | source = '~/dotfiles/zt/slop' 164 | solution = 'cd ~/dotfiles/zt/slop; make clean; cmake -DCMAKE_INSTALL_PREFIX="/usr" ./ && make && sudo make install' 165 | method = 'execute' 166 | os = 'linux::debian' 167 | 168 | [[obj]] 169 | task = 'maim' 170 | source = '~/dotfiles/zt/maim' 171 | solution = 'cd ~/dotfiles/zt/maim; make clean; cmake -DCMAKE_INSTALL_PREFIX="/usr" ./ && make && sudo make install' 172 | method = 'execute' 173 | dependencies = ['maim_dependencies', 'slop'] 174 | os = 'linux::debian' 175 | 176 | [[obj]] 177 | task = 'maim_dependencies' 178 | solution = 'sudo apt install -y libxfixes-dev libglm-dev libxrandr-dev libglew-dev libegl1-mesa-dev libxcomposite-dev' 179 | # os should be of the format either `windows` or `linux::` 180 | os = "linux::debian" 181 | 182 | [[obj]] 183 | task = 'windows only' 184 | solution = 'rustup update' 185 | os = 'windows' 186 | 187 | [[obj]] 188 | task = 'rustup update' 189 | solution = 'rustup update' 190 | method = 'execute' 191 | 192 | [[obj]] 193 | task = 'loop_detection' 194 | solution = 'echo HI' 195 | #dependencies = 'maim_dependencies' 196 | 197 | [[obj]] 198 | task = 'playerctl dependencies' 199 | solution = 'sudo pip3 install meson' 200 | os = "linux::fedora" 201 | 202 | [[obj]] 203 | task = 'playerctl install' 204 | solution = 'test -e ~/src/playerctl || mkdir -p ~/src; cd ~/src/; git clone https://github.com/altdesktop/playerctl.git ; cd playerctl; meson -Dgtk-doc=false -Dintrospection=false mesonbuild && sudo ninja -C mesonbuild install' 205 | dependencies = ['playerctl dependencies'] 206 | os = "linux::fedora" 207 | 208 | 209 | # BUILDING SWAY 210 | # build libdrm ~/src/drm 211 | # build wlroots ~/src/sway/subprojects/wlroots 212 | # build sway ~/src/sway 213 | 214 | [[obj]] 215 | task = 'libdrm' 216 | solution = '(test -d ~/src/drm || git clone --recursive https://gitlab.freedesktop.org/mesa/drm.git ~/src/drm) && cd ~/src/drm && meson build && ninja -C build' 217 | os = "linux::fedora" 218 | 219 | [[obj]] 220 | task = 'clone wlroots' 221 | solution = 'test -d ~/src/sway/subprojects/wlroots || git clone https://gitlab.freedesktop.org/wlroots/wlroots.git ~/src/sway/subprojects/wlroots' 222 | dependencies = ['clone swaywm'] 223 | os = "linux::fedora" 224 | 225 | [[obj]] 226 | task = 'build wlroots' 227 | solution = 'cd ~/src/sway/subprojects/wlroots/ ; meson build && ninja -C build' 228 | dependencies = ['clone wlroots', 'clone swaywm'] 229 | os = "linux::fedora" 230 | 231 | [[obj]] 232 | task = 'clone swaywm' 233 | solution = '((test -d ~/src/sway && cd ~/src/sway && git -C ~/src/sway rev-parse 2>/dev/null && git pull) || echo "cloning sway..." ; git clone --recursive https://github.com/swaywm/sway.git ~/src/sway) && (test -d ~/src/sway/subprojects || mkdir ~/src/sway/subprojects)' 234 | os = "linux::fedora" 235 | 236 | [[obj]] 237 | task = 'clone wayland-protocols' 238 | solution = 'test -d ~/src/sway/subprojects/wayland-protocols || git clone https://gitlab.freedesktop.org/wayland/wayland-protocols.git ~/src/sway/subprojects/wayland-protocols' 239 | dependencies = ['clone swaywm'] 240 | 241 | [[obj]] 242 | task = 'clone seatd' 243 | solution='test -d ~/src/sway/subprojects/seatd || git clone https://git.sr.ht/~kennylevinsen/seatd ~/src/sway/subprojects/seatd' 244 | dependencies = ['clone swaywm'] 245 | 246 | [[obj]] 247 | task = 'clone and build wlsunset' 248 | solution = 'test -e /usr/local/bin/wlsunset || (mkdir -p ~/src; git clone --recursive https://git.sr.ht/~kennylevinsen/wlsunset ~/src/wlsunset ; cd ~/src/wlsunset ; meson build && ninja -C build)' 249 | os = "linux::fedora" 250 | 251 | [[obj]] 252 | task = 'rpmfusion' 253 | solution = 'sudo dnf -y install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm && sudo dnf -y install https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm' 254 | os = "linux::fedora" 255 | 256 | [[obj]] 257 | task = 'swaywm' 258 | solution = 'cd ~/src/sway && meson build && sudo ninja -C build/ install' 259 | dependencies = ['swaywm_dependencies_fedora', 'clone swaywm', 'clone wlroots', 'clone wayland-protocols', 'clone seatd'] 260 | os = "linux::fedora" 261 | 262 | [[obj]] 263 | task = 'waybar deps' 264 | solution = 'sudo dnf install -y pulseaudio-libs-devel libappindicator-devel libappindicator-gtk3-devel pulseaudio-libs-devel libdbusmenu libdbusmenu-devel libdbusmenu-gtk3-devel' 265 | os = "linux::fedora" 266 | 267 | [[obj]] 268 | task = 'fedora deps' 269 | solution = "sudo dnf install -y light network-manager-applet" 270 | os = "linux::fedora" 271 | 272 | [[obj]] 273 | task = 'sleep kill ssh' 274 | solution = 'sudo cp ~/dotfiles/sleep.service && sudo systemctl enable sleep' 275 | os = "linux::fedora" 276 | 277 | [[obj]] 278 | task = 'swaywm_dependencies_fedora' 279 | solution = 'sudo dnf install -y libxkbcommon-devel cmake mesa-libgbm-devel libdrm-devel wayland-devel systemd-devel pixman-devel wayland-protocols-devel libxcb-devel xcb-util-devel ffmpeg-devel libxkbcommon-devel mesa-libgbm-devel libdrm-devel wayland-devel ffmpeg-devel json-c-devel pango-devel libdevdev-devel libglvnd-devel libinput-devel xorg-x11-server-Xwayland-devel xcb-util-wm-devel pulseaudio-utils libnl-devel' 280 | dependencies = ["rpmfusion"] 281 | os = "linux::fedora" 282 | 283 | [[obj]] 284 | task = 'redshift deps' 285 | solution = 'sudo dnf install -y gettext-devel libtool autoconf automake' 286 | os = "linux::fedora" 287 | -------------------------------------------------------------------------------- /src/hm.rs: -------------------------------------------------------------------------------- 1 | //! hm is a commandline program to help with dotfile (and more) management. 2 | //! 3 | //! It can handle putting configuration files where they should go, but its real 4 | //! strength lies in the solutions it'll execute - shell scripts, usually - to 5 | //! pull a git repository, compile something from source, etc. 6 | //! 7 | //! `hm` exists because I bring along a few utilities to all Linux boxes I regularly 8 | //! use, and those are often built from source. So rather than manually install 9 | //! all dependency libraries, then build each dependent piece, then finally the 10 | //! top-level dependent program, I built hm. 11 | 12 | //! 13 | //! 14 | //! have a dotfiles directory with all your stuff in it? have homemaker put everything in its right place. 15 | //! 16 | //! [gfycat of it in action](https://gfycat.com/skinnywarmheartedafricanpiedkingfisher) 17 | //! 18 | //! 19 | //! 1. create a config.toml file either anywhere or in ~/.config/homemaker/. 20 | //! 2. enter things to do things to in the file. 21 | //! example: 22 | //! ``` text 23 | //! ## config.toml 24 | //! 25 | //! [[obj]] 26 | //! file = 'tmux.conf' 27 | //! source = '~/dotfiles/.tmux.conf' 28 | //! destination = '~/.tmux.conf' 29 | //! method = 'symlink' 30 | //! 31 | //! [[obj]] 32 | //! task = 'zt' 33 | //! solution = 'cd ~/dotfiles/zt && git pull' 34 | //! dependencies = 'maim, slop' 35 | //! 36 | //! [[obj]] 37 | //! task = 'slop' 38 | //! source = '~/dotfiles/zt/slop' 39 | //! solution = 'cd ~/dotfiles/zt/slop; make clean; cmake -DCMAKE_INSTALL_PREFIX="/usr" ./ && make && sudo make install' 40 | //! method = 'execute' 41 | //! ``` 42 | //! 3. `hm ~/path/to/your/config.toml` 43 | //! 44 | //! [![built with spacemacs](https://cdn.rawgit.com/syl20bnr/spacemacs/442d025779da2f62fc86c2082703697714db6514/assets/spacemacs-badge.svg)](http://spacemacs.org) and neovim. 45 | //! 46 | //! thanks to actual good code: 47 | //! serde 48 | //! toml 49 | //! symlink 50 | //! solvent 51 | //! indicatif 52 | //! console 53 | 54 | use ::hm::{ 55 | config::{deserialize_file, ensure_config_dir, Config}, 56 | do_tasks, hmerror, 57 | }; 58 | use chrono::prelude::*; 59 | use indicatif::HumanDuration; 60 | use log::{info, warn}; 61 | use simplelog::{ConfigBuilder, LevelFilter, WriteLogger}; 62 | use std::{env, fs::File, path::PathBuf, process::exit, string::String, time::Instant}; 63 | 64 | /// Pull apart our arguments, if they're called, get our Config, and error-check. 65 | /// Then work our way through the Config, executing the easy stuff, and threading off the hard. 66 | fn main() { 67 | let args: Vec = env::args().collect(); 68 | // it's a little hackish, but we don't have to bring in an external crate to do our args 69 | let mut target_task: Option = None; 70 | let mut arg_config: Option = None; 71 | for i in 0..args.len() { 72 | match args[i].as_str() { 73 | "log" | "logs" => { 74 | println!("{}", recent_log_path()); 75 | exit(0) 76 | } 77 | "-t" | "--task" => { 78 | if args.len() > i && !args[i + 1].starts_with('-') { 79 | // ensure the next arg is not a flag 80 | // assume the next one is the named task to complete 81 | target_task = Some(args[i + 1].clone()); 82 | } else { 83 | hmerror::error( 84 | "-t flag requires specified task immediately after", 85 | "No task was specified.", 86 | ); 87 | help(); 88 | } 89 | } 90 | "clean" | "--clean" => { 91 | match clean() { 92 | Ok(_) => { 93 | exit(0); 94 | } 95 | Err(e) => { 96 | eprintln!("{}", e); 97 | exit(0); 98 | } 99 | }; 100 | } 101 | "-c" | "--config" if args.len() > i + 1 => { 102 | arg_config = Some(args[i + 1].clone()); 103 | } 104 | "-h" | "--help" => { 105 | help(); 106 | } 107 | _ => {} 108 | } 109 | } 110 | let l = Local::now(); 111 | let mut slc = ConfigBuilder::new(); 112 | let _ = slc.set_time_offset_to_local(); 113 | let mut p = "./logs/".to_string(); 114 | let mut log_file_name = String::from("hm-task-"); 115 | // we don't really care if we can make the directory. if we can, great. 116 | match std::fs::create_dir("./logs/") { 117 | Ok(_) => {} 118 | Err(e) => { 119 | warn!("Couldn't create log directory :( . Error: {}", e); 120 | } 121 | }; 122 | log_file_name.push_str(l.to_string().as_str()); 123 | log_file_name.push_str(".log"); 124 | p.push_str(log_file_name.as_str()); 125 | // nifty thing is we can make it here, and then we _never_ 126 | // have to pass it around - singleton. just info!, trace!, warn!, etc 127 | let _ = WriteLogger::init(LevelFilter::Trace, slc.build(), File::create(p).unwrap()); 128 | info!("beginning hm execution..."); 129 | /* 130 | accept either a config passed in specifically (with -c or --config) or try to open the default config location 131 | */ 132 | let a: Config = match arg_config { 133 | Some(second) => match deserialize_file(&second) { 134 | Ok(c) => c, 135 | Err(e) => { 136 | hmerror::error( 137 | format!("Couldn't open specified config file `{}`", &second).as_str(), 138 | e.to_string().as_str(), 139 | ); 140 | exit(1) 141 | } 142 | }, 143 | None => { 144 | let _p: PathBuf = ensure_config_dir() 145 | .map_err(|e| { 146 | hmerror::error("Couldn't ensure config dir: {}", e); 147 | exit(1); 148 | }) 149 | .unwrap(); 150 | match deserialize_file(_p.to_str().unwrap()) { 151 | Ok(c) => c, 152 | Err(e) => { 153 | hmerror::error( 154 | format!( 155 | "Couldn't open assumed (unspecified) config file {}", 156 | _p.to_string_lossy() 157 | ) 158 | .as_str(), 159 | e.to_string().as_str(), 160 | ); 161 | exit(1) 162 | } 163 | } 164 | } 165 | }; 166 | // do it here 167 | let started = Instant::now(); 168 | match do_tasks(Config::as_managed_objects(a), target_task) { 169 | Ok(_) => { 170 | println!("Done in {}.", HumanDuration(started.elapsed())); 171 | exit(0); 172 | } 173 | Err(e) => { 174 | hmerror::error(format!("{}", e).as_str(), "poooooop"); 175 | exit(3); 176 | } 177 | } 178 | } 179 | 180 | fn recent_log_path() -> String { 181 | let contents: Vec = std::fs::read_dir("./logs/") 182 | .map(|res| res.map(|e| e.expect("AIYEEEE").path())) 183 | .expect("FURTHER AIYEEE") 184 | .collect::>(); 185 | 186 | /* 187 | we use a btreemap instead of a hashmap because it's out of the box sorted, 188 | which makes getting our last item possible 189 | on fedora 35 as of Sat 08 Oct 2022 09:12:47 AM PDT it comes out ordered properly from read_dir, 190 | so a hashmap would have been fine, but it isn't guaranteed 191 | */ 192 | 193 | let mut files: std::collections::BTreeMap = 194 | std::collections::BTreeMap::new(); 195 | for item in &contents { 196 | let m = item.metadata(); 197 | files.insert(m.unwrap().accessed().unwrap(), item); 198 | } 199 | files 200 | .iter() 201 | .next_back() 202 | .unwrap() 203 | .1 204 | .as_os_str() 205 | .to_string_lossy() 206 | .to_string() 207 | } 208 | 209 | /// Clean up our logs directory. 210 | fn clean() -> std::io::Result<()> { 211 | std::fs::remove_dir_all("./logs/")?; 212 | Ok(()) 213 | } 214 | 215 | /// Print help for the user. 216 | fn help() { 217 | println!( 218 | "usage: 219 | hm [-h] | [-t|--task] [] | --clean | [-c|--config] [] 220 | -t | --task > run specific named task 221 | -h | --help > this help message 222 | clean > removes the contents of the log directory 223 | log > return the path of the most recent log file (use with your editor - `nvr (hm log)`) 224 | -c | --config [config] > Optional. 225 | if config is not specified, default location of ~/.config/homemaker/config.toml is assumed." 226 | ); 227 | exit(0) 228 | } 229 | -------------------------------------------------------------------------------- /src/lib/config.rs: -------------------------------------------------------------------------------- 1 | //! Define our `Config` and `Worker`. 2 | //! Implement how to take the `config.toml` and turn it into a `Config { files: Vec }`. 3 | //! Describes the `Worker` object, which is how we communicate back to our `main()` thread 4 | //! about how our `task` is going. 5 | extern crate serde; 6 | extern crate strum_macros; 7 | extern crate toml; 8 | 9 | use serde::{Deserialize, Deserializer, Serialize}; 10 | use std::collections::HashMap; 11 | use std::str::FromStr; 12 | use std::{ 13 | fmt, fs, 14 | io::{self, prelude::*, BufReader}, 15 | path::{Path, PathBuf}, 16 | string::String, 17 | }; 18 | //use strum; 19 | use strum_macros::EnumString; 20 | use toml::value; 21 | 22 | use super::hmerror::{HMError, Result as HMResult}; 23 | 24 | /// 25 | /// Allow us to communicate meaningfully back to `main()` thread. 26 | /// 27 | #[derive(Debug, Clone)] 28 | pub struct Worker { 29 | pub name: String, 30 | pub status: Option, 31 | pub completed: bool, 32 | } 33 | 34 | impl<'a> Worker { 35 | // i'll get to you 36 | #[allow(dead_code)] 37 | pub fn new() -> Worker { 38 | Worker { 39 | name: String::from(""), 40 | status: Some(1), 41 | completed: false, 42 | } 43 | } 44 | } 45 | 46 | impl<'a> Default for Worker { 47 | fn default() -> Self { 48 | Self::new() 49 | } 50 | } 51 | 52 | impl PartialEq for Worker { 53 | fn eq(&self, other: &Self) -> bool { 54 | self.name == other.name 55 | } 56 | } 57 | impl Eq for Worker {} 58 | 59 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd, Ord, Hash, EnumString)] 60 | #[strum(serialize_all = "snake_case")] 61 | pub enum OS { 62 | Windows, 63 | Unknown, 64 | Linux(LinuxDistro), 65 | } 66 | 67 | impl Default for OS { 68 | fn default() -> Self { 69 | OS::Unknown 70 | } 71 | } 72 | 73 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Hash, EnumString)] 74 | #[strum(serialize_all = "snake_case")] 75 | pub enum LinuxDistro { 76 | Fedora, 77 | Debian, 78 | Ubuntu, 79 | Arch, 80 | Generic, 81 | } 82 | 83 | impl Default for LinuxDistro { 84 | fn default() -> Self { 85 | LinuxDistro::Generic 86 | } 87 | } 88 | 89 | impl Eq for OS {} 90 | 91 | impl Eq for LinuxDistro {} 92 | 93 | /// 94 | /// Windows or Linux? If Linux, let's determine our distro, because package managers and stuff. 95 | /// 96 | pub fn determine_os() -> OS { 97 | match sys_info::os_type() { 98 | Ok(s) => match s.to_ascii_lowercase().as_str() { 99 | "linux" => match sys_info::linux_os_release() { 100 | Ok(l) => { 101 | let a: String = l.name.unwrap().to_ascii_lowercase(); 102 | if a.contains("fedora") { 103 | OS::Linux(LinuxDistro::Fedora) 104 | } else if a.contains("debian") { 105 | OS::Linux(LinuxDistro::Debian) 106 | } else if a.contains("ubuntu") { 107 | OS::Linux(LinuxDistro::Ubuntu) 108 | } else { 109 | OS::Unknown 110 | } 111 | } 112 | Err(_e) => OS::Unknown, 113 | }, 114 | "windows" => OS::Windows, 115 | _ => OS::Unknown, 116 | }, 117 | Err(_e) => OS::Unknown, 118 | } 119 | } 120 | 121 | /// We're a super-set of all the kinds of `ManagedObject`s we can be. 122 | /// Just don't use the fields you don't wanna use. 123 | /// A simple `ManagedObject` is a name, source, destination, and method (currently only symlink). 124 | /// The simple `ManagedObject` would just be symlinked to its destination. 125 | /// Complex `ManagedObject`s include solutions, which are executed scripts. 126 | #[derive(Serialize, Deserialize, Debug, Clone)] 127 | pub struct ManagedObject { 128 | pub name: String, 129 | pub source: String, 130 | pub file: String, 131 | pub destination: String, 132 | pub method: String, 133 | pub task: String, 134 | pub solution: String, 135 | pub dependencies: Vec, 136 | pub satisfied: bool, 137 | pub os: Option, 138 | pub force: bool, 139 | pub post: String, 140 | } 141 | 142 | impl ManagedObject { 143 | /// quite simply, if we're a task, we'll have a `solution`. 144 | pub fn is_task(&self) -> bool { 145 | !self.solution.is_empty() 146 | } 147 | pub fn set_satisfied(&mut self) { 148 | self.satisfied = true; 149 | } 150 | } 151 | impl PartialEq for ManagedObject { 152 | fn eq(&self, other: &Self) -> bool { 153 | if self.name == other.name 154 | && self.source == other.source 155 | && self.destination == other.destination 156 | && self.task == other.task 157 | && self.solution == other.solution 158 | { 159 | return true; 160 | } 161 | false 162 | } 163 | } 164 | 165 | impl fmt::Display for ManagedObject { 166 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 167 | write!( 168 | f, 169 | "{} {} {} {} {} {} {} {} {:?} {:?} {}", 170 | self.name, 171 | self.file, 172 | self.source, 173 | self.method, 174 | self.destination, 175 | self.task, 176 | self.solution, 177 | self.satisfied, 178 | self.os, 179 | self.force, 180 | self.post 181 | ) 182 | } 183 | } 184 | 185 | impl Default for ManagedObject { 186 | fn default() -> Self { 187 | ManagedObject { 188 | name: String::from(""), 189 | source: String::from(""), 190 | file: String::from(""), 191 | destination: String::from(""), 192 | method: String::from(""), 193 | task: String::from(""), 194 | solution: String::from(""), 195 | dependencies: Vec::new(), 196 | satisfied: false, 197 | os: None, 198 | force: false, 199 | post: "".to_string(), 200 | } 201 | } 202 | } 203 | 204 | /// Represents just the file `config.toml` and contains a vector of things 205 | /// that shall become `ManagedObject`s. 206 | #[derive(Deserialize, Clone, Default)] 207 | pub struct Config { 208 | #[serde(rename = "obj", deserialize_with = "deserialize_files")] 209 | pub files: Vec<(String, value::Value)>, 210 | } 211 | 212 | /* 213 | impl Default for Config { 214 | fn default() -> Self { 215 | Config { files: Vec::new() } 216 | } 217 | } 218 | */ 219 | 220 | impl fmt::Display for Config { 221 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 222 | let mos = Config::as_managed_objects(self.clone()); 223 | write!(f, "{:#?}", mos) 224 | } 225 | } 226 | 227 | /// Represents our Config file, which is just a big list of `[[obj]]`s 228 | impl Config { 229 | /// Allows us to get a specified Managed Object by name 230 | #[allow(dead_code)] 231 | pub fn get_mo(&mut self, _n: &str) -> Option { 232 | Config::as_managed_objects(self.clone()) 233 | .get(_n) 234 | .map(|a| a.to_owned()) 235 | } 236 | 237 | /// Convenience function that allows getting a HashMap from a `Config` of 238 | /// the `ManagedObject`s within. 239 | pub fn as_managed_objects(config: Config) -> HashMap { 240 | config 241 | .files 242 | .iter() 243 | .map(|(name, val)| { 244 | let mut mo = ManagedObject { 245 | name: name.to_owned(), 246 | ..Default::default() 247 | }; 248 | mo.name = name.to_owned(); 249 | if let Some(_x) = val.get("solution") { 250 | mo.solution = String::from(_x.as_str().unwrap()); 251 | } 252 | if let Some(_x) = val.get("task") { 253 | mo.task = String::from(_x.as_str().unwrap()); 254 | } 255 | if let Some(_x) = val.get("source") { 256 | mo.source = String::from(_x.as_str().unwrap()); 257 | } 258 | if let Some(_x) = val.get("method") { 259 | mo.method = String::from(_x.as_str().unwrap()); 260 | } 261 | if let Some(_x) = val.get("destination") { 262 | mo.destination = String::from(_x.as_str().unwrap()); 263 | } 264 | if let Some(_x) = val.get("dependencies") { 265 | let _f = _x.as_array().unwrap(); 266 | mo.dependencies = _f.iter().map(|v| v.as_str().unwrap().to_owned()).collect(); 267 | } 268 | if let Some(_x) = val.get("force") { 269 | mo.force = matches!(_x.as_str().unwrap(), "true"); 270 | } 271 | if let Some(_x) = val.get("post") { 272 | mo.post = String::from(_x.as_str().unwrap()); 273 | } 274 | // 275 | // the `os =` entry in the config will be formatted either 276 | // `windows` or `linux::` 277 | if let Some(_x) = val.get("os") { 278 | let _b = String::from(_x.as_str().unwrap()); 279 | let _a: Vec<&str> = _b.split("::").collect::>(); 280 | // TODO change this to match on strum's VariantNotFound 281 | mo.os = if _a.len() > 1 { 282 | Some(OS::Linux( 283 | LinuxDistro::from_str(_a[1].to_lowercase().as_str()).unwrap(), 284 | )) 285 | } else { 286 | Some(OS::from_str(_a[0].to_lowercase().as_str()).unwrap()) 287 | }; 288 | } else { 289 | mo.os = None; 290 | } 291 | (mo.name.clone(), mo) 292 | }) 293 | .collect() 294 | } 295 | } 296 | 297 | /// This takes our file/task array and turns them into `ManagedObjects`, 298 | /// to be stuffed into the `Config`. 299 | pub fn deserialize_files<'de, D>(deserializer: D) -> Result, D::Error> 300 | where 301 | D: Deserializer<'de>, 302 | { 303 | let mut files: Vec<(String, value::Value)> = Vec::new(); 304 | let raw_files: Vec = Deserialize::deserialize(deserializer)?; 305 | for mut entry in raw_files { 306 | if let Some(name) = entry.remove("file") { 307 | if let Some(name) = name.as_str() { 308 | files.push((name.to_owned(), value::Value::Table(entry))); 309 | } 310 | } else if let Some(name) = entry.remove("task") { 311 | if let Some(name) = name.as_str() { 312 | files.push((name.to_owned(), value::Value::Table(entry))); 313 | } 314 | } 315 | } 316 | Ok(files) 317 | } 318 | 319 | /// Take a big, fat guess. 320 | /// Open the specified file. We've already made sure our Path and stuff 321 | /// look good. 322 | fn open_config(file: &str) -> io::Result { 323 | fs::File::open(file) 324 | } 325 | 326 | /// Open our config file and read the entire contents into hopefully 327 | /// valid toml. Either we gucci and return back a `Config` made of toml, 328 | /// or we explain what went wrong with the `toml` Err. 329 | pub fn deserialize_file(file: &str) -> HMResult { 330 | let mut contents = String::new(); 331 | let g = match open_config(file) { 332 | Ok(_a) => _a, 333 | Err(e) => return Err(HMError::Other(e.to_string())), 334 | }; 335 | 336 | let mut file_contents = BufReader::new(g); 337 | match file_contents.read_to_string(&mut contents) { 338 | Ok(v) => v, 339 | Err(_e) => 0, 340 | }; 341 | if cfg!(debug_assertions) { 342 | println!("file: {}", &file); 343 | } 344 | deserialize_str(&contents) 345 | } 346 | 347 | fn deserialize_str(contents: &str) -> HMResult { 348 | toml::from_str(contents).map_err(|e| HMError::Other(e.to_string())) 349 | } 350 | 351 | /// Make sure $XDG_CONFIG_DIR exists. 352 | /// On Linux and similar this is /home/\/.config; 353 | /// macOS /Users/\/.config, 354 | /// Windows C:\\Users\\\\\.config 355 | /// 356 | /// Assuming it does exist or we can create it, stuff config.toml on the end of it 357 | /// and return `Ok(my_path_buf/config.toml)`. 358 | /// 359 | /// This is safe because `ensure_config_dir()` is called only after we already know 360 | /// the user didn't specify a `config.toml` path themselves. We must check our 361 | /// default expected location for it. 362 | pub fn ensure_config_dir() -> Result { 363 | // get /home//.config, if exists... 364 | match dirs_next::config_dir() { 365 | Some(p) => { 366 | // if something 367 | // creates a PathBuf from $XDG_CONFIG_DIR 368 | let whole_path = p.join(Path::new("homemaker")); 369 | match fs::create_dir_all(&whole_path) { 370 | /* 371 | then when we return it, do the entire config dir path (/home/hlmtre/.config) 372 | and add our config file to the end of the PathBuf 373 | ``` 374 | Ok(("/home/hlmtre/.config").join("config.toml")) 375 | ``` 376 | sort of as a pseudocodey example 377 | */ 378 | Ok(()) => Ok(PathBuf::from(&whole_path.join("config.toml"))), 379 | Err(_e) => Err("Couldn't create config dir!"), 380 | } 381 | } 382 | // if dirs::config_path() call doesn't return anything 383 | None => Err("Couldn't get config directory from $XDG"), 384 | } 385 | } 386 | 387 | #[cfg(test)] 388 | mod config_test { 389 | use super::*; 390 | 391 | /* 392 | source looks like this: 393 | [[obj]] 394 | file = 'tmux.conf' 395 | source = '~/dotfiles/.tmux.conf' 396 | destination = '~/.tmux.conf' 397 | method = 'symlink' 398 | */ 399 | #[test] 400 | fn test_mo_deserialization() { 401 | let mut a: Config = deserialize_file("./benches/config.toml").unwrap(); 402 | let mo = ManagedObject { 403 | name: "tmux.conf".to_string(), 404 | source: "~/dotfiles/.tmux.conf".to_string(), 405 | destination: "~/.tmux.conf".to_string(), 406 | method: "symlink".to_string(), 407 | ..Default::default() 408 | }; 409 | assert_eq!(mo, a.get_mo("tmux.conf").unwrap()); 410 | } 411 | 412 | #[test] 413 | fn dependencies_is_array() { 414 | let mut a: Config = deserialize_str( 415 | r#" 416 | [[obj]] 417 | task = 'zt' 418 | solution = 'cd ~/dotfiles/zt && git pull' 419 | dependencies = ['grim', 'slurp'] 420 | "#, 421 | ) 422 | .unwrap(); 423 | assert_eq!(vec!["grim", "slurp"], a.get_mo("zt").unwrap().dependencies); 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /src/lib/hm_macro.rs: -------------------------------------------------------------------------------- 1 | /// equivalent of __func__ for stacktrace/debugging 2 | /// see https://stackoverflow.com/questions/38088067/equivalent-of-func-or-function-in-rust 3 | #[macro_export] 4 | macro_rules! function { 5 | () => {{ 6 | fn f() {} 7 | fn type_name_of(_: T) -> &'static str { 8 | std::any::type_name::() 9 | } 10 | let name = type_name_of(f); 11 | &name[..name.len() - 3] 12 | }}; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/hmerror.rs: -------------------------------------------------------------------------------- 1 | //! Custom Error implementation to allow for our own kind, along with 2 | //! run-of-the-mill `std::io::Error`s. 3 | //! ``` 4 | //! pub enum ErrorKind { 5 | //! DependencyUndefinedError, 6 | //! CyclicalDependencyError, 7 | //! SolutionError, 8 | //! ConfigError, 9 | //! Other, 10 | //! } 11 | //! ``` 12 | //! * DependencyUndefinedError: A stated dependency doesn't have an object telling us how to satisfy it. 13 | //! * CyclicalDependencyError: a -> b and b -> a and neither is satisfied. The offending object is the tippy-top of the chain. 14 | //! * SolutionError: Something went wrong in our script. 15 | //! * ConfigError: Something is wrong with how you wrote the `config.toml`. 16 | //! * Other: Other. 17 | extern crate console; 18 | extern crate serde; 19 | use console::style; 20 | use std::fmt; 21 | use std::io; 22 | 23 | use crate::config; 24 | 25 | #[derive(Debug)] 26 | pub enum HMError { 27 | Io(io::Error), 28 | Regular(ErrorKind), 29 | Other(String), 30 | } 31 | 32 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 33 | pub enum ErrorKind { 34 | DependencyUndefinedError { 35 | dependency: String, 36 | }, 37 | IncorrectPlatformError { 38 | dependency: String, 39 | platform: config::OS, 40 | target_platform: config::OS, 41 | }, 42 | CyclicalDependencyError { 43 | dependency_graph: String, 44 | }, 45 | SolutionError { 46 | solution: String, 47 | }, 48 | ConfigError { 49 | line_number: usize, 50 | }, 51 | Other, 52 | } 53 | 54 | impl From for HMError { 55 | fn from(err: io::Error) -> HMError { 56 | HMError::Io(err) 57 | } 58 | } 59 | 60 | /* 61 | impl From for HMError { 62 | fn from(err: Deserializer::Error) -> HMError { 63 | HMError::Other(err) 64 | } 65 | } 66 | */ 67 | 68 | impl ErrorKind { 69 | fn as_str(&self) -> &str { 70 | match *self { 71 | ErrorKind::ConfigError { line_number: _ } => "configuration error", 72 | ErrorKind::SolutionError { solution: _ } => "solution error", 73 | ErrorKind::DependencyUndefinedError { dependency: _ } => "dependency undefined", 74 | ErrorKind::IncorrectPlatformError { 75 | dependency: _, 76 | platform: _, 77 | target_platform: _, 78 | } => "incorrect platform for dependency", 79 | ErrorKind::CyclicalDependencyError { 80 | dependency_graph: _, 81 | } => "cyclical dependency", 82 | ErrorKind::Other => "other error", 83 | } 84 | } 85 | } 86 | 87 | impl fmt::Display for HMError { 88 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 89 | match *self { 90 | HMError::Regular(ref err) => write!(f, "{:?}", err), 91 | HMError::Other(ref err) => write!(f, "{:?}", err), 92 | HMError::Io(ref err) => err.fmt(f), 93 | } 94 | } 95 | } 96 | 97 | pub fn happy_print(jubilation: &str) { 98 | println!("{}!", style(jubilation).green().bold()) 99 | } 100 | 101 | /// Easy formatting for errors as they come in. 102 | /// 103 | /// example: 104 | /// 105 | /// ``` 106 | /// use hm::hmerror; 107 | /// let _a = "src/config.toml"; 108 | /// let _e = "my_dummy_error"; 109 | /// hmerror::error( 110 | /// format!("Couldn't open specified config file `{}`", _a).as_str(), 111 | /// _e, 112 | /// ); 113 | /// ``` 114 | pub fn error(complaint: &str, er: &str) { 115 | eprintln!("{}:\n ↳ Error: {}", style(complaint).red().bold(), er) 116 | } 117 | 118 | pub type Result = std::result::Result; 119 | -------------------------------------------------------------------------------- /src/lib/mod.rs: -------------------------------------------------------------------------------- 1 | //! `hm` is a commandline program to help with dotfile (and more) management. 2 | //! 3 | //! It can handle putting configuration files where they should go, but its real 4 | //! strength lies in the solutions it'll execute - shell scripts, usually - to 5 | //! pull a git repository, compile something from source, etc. 6 | //! 7 | //! `hm` exists because I bring along a few utilities to all Linux boxes I regularly 8 | //! use, and those are often built from source. So rather than manually install 9 | //! all dependency libraries, then build each dependent piece, then finally the 10 | //! top-level dependent program, I built hm. 11 | //! It also provides functionality to thread off heavier operations 12 | //! into task threads, which regularly report back their status with 13 | //! Worker objects over a `std::sync::mpsc`. 14 | //! 15 | //! `hm` can also perform dependency resolution, thanks to the solvent crate. You can 16 | //! provide a big ol' list of tasks to complete, each with their own dependencies, and 17 | //! as long as you have a solveable dependency graph, you can get workable batches from 18 | //! get_task_batches(). They will be in *some* order that will resolve your dependencies, 19 | //! but that order is non-deterministic - if there's multiple ways to solve a graph, 20 | //! you'll probably get all those different ways back if you run it multiple times. 21 | //! 22 | //! The crate provides this library, which is in turn used by the bin `hm` (in `src/bin/main.rs`). 23 | //! `hm` is a commandline program to help with dotfile (and more) management. 24 | //! 25 | //! [gfycat of it in action](https://gfycat.com/skinnywarmheartedafricanpiedkingfisher) 26 | //! 27 | //! 28 | //! 1. create a config.toml file either anywhere or in ~/.config/homemaker/. 29 | //! 2. enter things to do things to in the file. 30 | //! example: 31 | //! ``` text 32 | //! ## config.toml 33 | //! 34 | //! [[obj]] 35 | //! file = 'tmux.conf' 36 | //! source = '~/dotfiles/.tmux.conf' 37 | //! destination = '~/.tmux.conf' 38 | //! method = 'symlink' 39 | //! 40 | //! [[obj]] 41 | //! task = 'zt' 42 | //! solution = 'cd ~/dotfiles/zt && git pull' 43 | //! dependencies = 'maim, slop' 44 | //! 45 | //! [[obj]] 46 | //! task = 'slop' 47 | //! source = '~/dotfiles/zt/slop' 48 | //! solution = 'cd ~/dotfiles/zt/slop; make clean; cmake -DCMAKE_INSTALL_PREFIX="/usr" ./ && make && sudo make install' 49 | //! method = 'execute' 50 | //! platform = "linux::debian" 51 | //! ``` 52 | //! 3. `hm ~/path/to/your/config.toml` 53 | //! 54 | //! [![built with spacemacs](https://cdn.rawgit.com/syl20bnr/spacemacs/442d025779da2f62fc86c2082703697714db6514/assets/spacemacs-badge.svg)](http://spacemacs.org) and neovim 55 | //! 56 | //! thanks to actual good code: 57 | //! serde 58 | //! toml 59 | //! symlink 60 | //! solvent 61 | //! indicatif 62 | //! console 63 | #![allow(clippy::many_single_char_names)] 64 | #![allow(dead_code)] 65 | #![allow(unused_macros)] 66 | extern crate console; 67 | extern crate indicatif; 68 | extern crate log; 69 | extern crate shellexpand; 70 | extern crate simplelog; 71 | extern crate solvent; 72 | extern crate symlink; 73 | extern crate sys_info; 74 | 75 | pub mod config; 76 | mod hm_macro; 77 | pub mod hmerror; 78 | 79 | use config::{ManagedObject, Worker}; 80 | use hmerror::{ErrorKind as hmek, HMError}; 81 | 82 | use console::{pad_str, style, Alignment}; 83 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 84 | use log::{info, warn}; 85 | use solvent::DepGraph; 86 | use std::{ 87 | collections::{HashMap, HashSet}, 88 | fmt, 89 | fs::{copy, create_dir_all, metadata, remove_dir_all, remove_file}, 90 | io::{BufRead, BufReader, Error, ErrorKind}, 91 | path::Path, 92 | process::{exit, Command, Stdio}, 93 | sync::mpsc::{self, Sender}, 94 | {thread, time}, 95 | }; 96 | use symlink::{symlink_dir as sd, symlink_file as sf}; 97 | 98 | /// I just wanna borrow one to look at it for a minute. 99 | /// use me with std::mem::transmute() 100 | /// absolutely not pub struct. don't look at me. 101 | #[derive(Debug, Clone)] 102 | struct SneakyDepGraphImposter { 103 | nodes: Vec, 104 | dependencies: HashMap>, 105 | satisfied: HashSet, 106 | } 107 | 108 | impl fmt::Display for SneakyDepGraphImposter { 109 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 110 | let mut i: usize = 0; 111 | for n in self.nodes.clone() { 112 | if self.dependencies.get(&i).is_some() { 113 | let _ = write!(f, "[ {} -> ", n); 114 | #[allow(clippy::for_loops_over_fallibles)] 115 | for d in self.dependencies.get(&i) { 116 | if d.is_empty() { 117 | write!(f, " ")?; 118 | } 119 | let mut j: usize = 1; 120 | for m in d { 121 | if j == d.len() { 122 | write!(f, "{} ", self.nodes[*m])?; 123 | } else { 124 | write!(f, "{}, ", self.nodes[*m])?; 125 | } 126 | j += 1; 127 | } 128 | } 129 | } 130 | i += 1; 131 | if i != self.nodes.len() { 132 | write!(f, "], ")?; 133 | } else { 134 | write!(f, "]")?; 135 | } 136 | } 137 | Ok(()) 138 | } 139 | } 140 | 141 | /// 142 | /// Copy our {file|directory} to the destination. Generally 143 | /// we'll be doing this in a tilde'd home subdirectory, so 144 | /// we need to be careful to get our Path right. 145 | /// 146 | pub fn copy_item(source: String, target: String, force: bool) -> Result<(), HMError> { 147 | let _lsource: String = shellexpand::tilde(&source).to_string(); 148 | let _ltarget: String = shellexpand::tilde(&target).to_string(); 149 | let md = match metadata(_lsource.clone()) { 150 | Ok(a) => a, 151 | Err(e) => return Err(HMError::Io(e)), 152 | }; 153 | if force && Path::new(_ltarget.as_str()).exists() { 154 | if md.is_dir() { 155 | remove_dir_all(_ltarget.clone())?; 156 | } else { 157 | remove_file(_ltarget.clone())?; 158 | } 159 | } 160 | copy(Path::new(_lsource.as_str()), Path::new(_ltarget.as_str()))?; 161 | Ok(()) 162 | } 163 | 164 | /// 165 | /// Either create a symlink to a file or directory. Generally 166 | /// we'll be doing this in a tilde'd home subdirectory, so 167 | /// we need to be careful to get our Path right. 168 | /// 169 | pub fn symlink_item(source: String, target: String, force: bool) -> Result<(), HMError> { 170 | let _lsource: String = shellexpand::tilde(&source).to_string(); 171 | let _ltarget: String = shellexpand::tilde(&target).to_string(); 172 | let md = match metadata(_lsource.clone()) { 173 | Ok(a) => a, 174 | Err(e) => return Err(HMError::Io(e)), 175 | }; 176 | let lmd = metadata(_ltarget.clone()); 177 | if force && Path::new(_ltarget.as_str()).exists() { 178 | // this is reasonably safe because if the target exists our lmd is a Result not an Err 179 | if lmd.unwrap().is_dir() { 180 | remove_dir_all(_ltarget.clone())?; 181 | } else { 182 | remove_file(_ltarget.clone())?; 183 | } 184 | } 185 | // create all the parent directories required for the target file/directory 186 | create_dir_all(Path::new(_ltarget.as_str()).parent().unwrap())?; 187 | if md.is_dir() { 188 | sd(Path::new(_lsource.as_str()), Path::new(_ltarget.as_str()))?; 189 | } else if md.is_file() { 190 | sf(Path::new(_lsource.as_str()), Path::new(_ltarget.as_str()))?; 191 | } 192 | Ok(()) 193 | } 194 | 195 | /// 196 | /// Take a ManagedObject task, an mpsc tx, and a Progressbar. Execute task and regularly inform the rx 197 | /// (all the way over back in `main()`)about our status using config::Worker. 198 | /// 199 | /// -TODO-: allow the `verbose` bool to show the output of the tasks as they go. 200 | /// Hey, it's done! Writes out to the logs/ directory. 201 | /// 202 | /// Return () or io::Error (something went wrong in our task). 203 | /// 204 | pub fn send_tasks_off_to_college( 205 | mo: &ManagedObject, 206 | tx: &Sender, 207 | p: ProgressBar, 208 | ) -> Result<(), Error> { 209 | let s: String = mo.solution.clone(); 210 | let s1: String = mo.solution.clone(); 211 | let n: String = mo.name.clone(); 212 | let tx1: Sender = Sender::clone(tx); 213 | let _: thread::JoinHandle> = thread::spawn(move || { 214 | let mut c = Command::new("bash") 215 | .arg("-c") 216 | .arg(s) 217 | .stdout(Stdio::piped()) 218 | .stderr(Stdio::piped()) 219 | .spawn() 220 | .unwrap(); 221 | let output: std::process::ChildStdout = c.stdout.take().unwrap(); 222 | let reader: BufReader = BufReader::new(output); 223 | // run this in another thread or the little block of tasks draws line-by-line 224 | // instead of all at once then updating as the task info gets Worker'd back 225 | thread::spawn(|| { 226 | reader 227 | .lines() 228 | .filter_map(|line| line.ok()) 229 | .for_each(|line| info!("{}", line)); 230 | }); 231 | p.set_style( 232 | ProgressStyle::default_spinner() 233 | .template("[{elapsed:4}] {prefix:.bold.dim} {spinner} {wide_msg}"), 234 | ); 235 | p.enable_steady_tick(200); 236 | let x = pad_str(format!("task {}", n).as_str(), 30, Alignment::Left, None).into_owned(); 237 | p.set_prefix(x); 238 | loop { 239 | let mut w: Worker = Worker { 240 | name: n.clone(), 241 | status: None, 242 | completed: false, 243 | }; 244 | match c.try_wait() { 245 | // we check each child status... 246 | Ok(Some(status)) => { 247 | if status.success() { 248 | // if we're done, send back :thumbsup: 249 | p.finish_with_message(console::style("✓").green().to_string()); 250 | w.status = status.code(); 251 | w.completed = true; 252 | tx1.send(w).unwrap(); 253 | info!("Successfully completed {}.", n); 254 | return Ok(()); 255 | } else { 256 | // or :sadface: 257 | drop(tx1); 258 | warn!("Error within `{}`", s1); 259 | p.abandon_with_message(console::style("✗").red().to_string()); 260 | return Err(HMError::Regular(hmek::SolutionError { solution: s1 })); 261 | } 262 | } // it's sent back nothing, not error, but not done 263 | Ok(None) => { 264 | tx1.send(w).unwrap(); 265 | thread::sleep(time::Duration::from_millis(200)); 266 | } 267 | Err(_e) => { 268 | // ahh send back err! 269 | drop(tx1); 270 | p.abandon_with_message(console::style("✗").red().to_string()); 271 | return Err(HMError::Regular(hmek::SolutionError { solution: s1 })); 272 | } 273 | } 274 | } 275 | }); 276 | Ok(()) 277 | } 278 | 279 | /* 280 | */ 281 | /// 282 | /// Create a non-cyclical dependency graph and give it back as a Vec<Vec<ManagedObject>>. 283 | /// Will return a CyclicalDependencyError if the graph is unsolveable. 284 | /// Intended to be used with either mgmt::execute_solution or mgmt::send_tasks_off_to_college. 285 | /// 286 | /// 287 | /// Example: 288 | /// 289 | /// ``` 290 | /// extern crate indicatif; 291 | /// use std::sync::mpsc; 292 | /// use std::collections::HashMap; 293 | /// use indicatif::{MultiProgress, ProgressBar}; 294 | /// use hm::config::ManagedObject; 295 | /// use hm::{get_task_batches, send_tasks_off_to_college}; 296 | /// let nodes: HashMap = HashMap::new(); 297 | /// let (tx, rx) = mpsc::channel(); 298 | /// let mp: MultiProgress = MultiProgress::new(); 299 | /// let v: Vec> = get_task_batches(nodes, None).unwrap(); 300 | /// for a in v { 301 | /// for b in a { 302 | /// let _p: ProgressBar = mp.add(ProgressBar::new_spinner()); 303 | /// send_tasks_off_to_college(&b, &tx, _p); 304 | /// } 305 | /// } 306 | /// ``` 307 | /// 308 | pub fn get_task_batches( 309 | mut nodes: HashMap, 310 | target_task: Option, 311 | ) -> Result>, HMError> { 312 | let our_os = config::determine_os(); 313 | let mut depgraph: DepGraph = DepGraph::new(); 314 | let mut nodes_to_remove: Vec = Vec::new(); 315 | let mut wrong_platforms: HashMap = HashMap::new(); 316 | for (name, node) in &nodes { 317 | if node.os.is_none() || node.os.clone().unwrap() == our_os { 318 | depgraph.register_dependencies(name.to_owned(), node.dependencies.clone()); 319 | } else { 320 | nodes_to_remove.push(name.to_string()); 321 | wrong_platforms.insert(name.clone(), node.os.clone().unwrap()); 322 | } 323 | } 324 | for n in nodes_to_remove { 325 | nodes.remove(&n); 326 | } 327 | let mut tasks: Vec> = Vec::new(); 328 | let mut _dedup: HashSet = HashSet::new(); 329 | /* 330 | ok. we've got a target task. we can get the depgraph for ONLY that task, 331 | and don't need to go through our entire config to solve for each. 332 | just the subtree involved in our target. 333 | */ 334 | if let Some(tt_name) = target_task { 335 | // TODO: break out getting our tasklist into its own function 336 | // de-dup me! 337 | let mut qtdg: Vec = Vec::new(); 338 | let tdg: solvent::DepGraphIterator = match depgraph.dependencies_of(&tt_name) { 339 | Ok(i) => i, 340 | Err(_) => { 341 | return Err(HMError::Regular(hmek::DependencyUndefinedError { 342 | dependency: tt_name, 343 | })); 344 | } 345 | }; 346 | for n in tdg { 347 | match n { 348 | Ok(r) => { 349 | let mut a = match nodes.get(r) { 350 | Some(a) => a, 351 | None => { 352 | /* 353 | if we have a dependency, but it can't be solved because it's for the incorrect platform, 354 | let's complain about it. 355 | doing it this way is necessary because we DO still want our dependency graph to get run. 356 | */ 357 | if wrong_platforms.contains_key(r) { 358 | return Err(HMError::Regular(hmek::IncorrectPlatformError { 359 | dependency: String::from(r), 360 | platform: our_os, 361 | target_platform: wrong_platforms.get(r).cloned().unwrap(), 362 | })); 363 | } else { 364 | return Err(HMError::Regular(hmek::DependencyUndefinedError { 365 | dependency: String::from(r), 366 | })); 367 | } 368 | } 369 | } 370 | .to_owned(); 371 | a.set_satisfied(); 372 | qtdg.push(a); 373 | } 374 | Err(_e) => unsafe { 375 | // we'll just borrow this for a second 376 | // just to look 377 | // i'm not gonna touch it i promise 378 | let my_sneaky_depgraph: SneakyDepGraphImposter = std::mem::transmute(depgraph); 379 | return Err(HMError::Regular(hmek::CyclicalDependencyError { 380 | // we can do this because we've implemented fmt::Display for SneakyDepGraphImposter 381 | dependency_graph: my_sneaky_depgraph.to_string(), 382 | })); 383 | }, 384 | } 385 | } 386 | tasks.push(qtdg); 387 | } else { 388 | // not doing target task 389 | // we're doing the entire config 390 | for name in nodes.keys() { 391 | let mut q: Vec = Vec::new(); 392 | let dg: solvent::DepGraphIterator = depgraph.dependencies_of(name).unwrap(); 393 | for n in dg { 394 | match n { 395 | Ok(r) => { 396 | let c = String::from(r.as_str()); 397 | // returns true if the set DID NOT have c in it already 398 | if _dedup.insert(c) { 399 | let mut a = match nodes.get(r) { 400 | Some(a) => a, 401 | None => { 402 | /* 403 | if we have a dependency, but it can't be solved because it's for the incorrect platform, 404 | let's complain about it. 405 | doing it this way is necessary because we DO still want our dependency graph to get run. 406 | */ 407 | if wrong_platforms.contains_key(r) { 408 | return Err(HMError::Regular(hmek::IncorrectPlatformError { 409 | dependency: String::from(r), 410 | platform: our_os, 411 | target_platform: wrong_platforms.get(r).cloned().unwrap(), 412 | })); 413 | } else { 414 | return Err(HMError::Regular(hmek::DependencyUndefinedError { 415 | dependency: String::from(r), 416 | })); 417 | } 418 | } 419 | } 420 | .to_owned(); 421 | a.set_satisfied(); 422 | q.push(a); 423 | } 424 | } 425 | Err(_e) => unsafe { 426 | // we'll just borrow this for a second 427 | // just to look 428 | // i'm not gonna touch it i promise 429 | let my_sneaky_depgraph: SneakyDepGraphImposter = std::mem::transmute(depgraph); 430 | return Err(HMError::Regular(hmek::CyclicalDependencyError { 431 | // we can do this because we've implemented fmt::Display for SneakyDepGraphImposter 432 | dependency_graph: my_sneaky_depgraph.to_string(), 433 | })); 434 | }, 435 | } 436 | } 437 | tasks.push(q); 438 | } 439 | } 440 | Ok(tasks) 441 | } 442 | 443 | /// 444 | /// Execute the shell commands specified in the MO's solution in a thread, so as not to block. 445 | /// 446 | fn execute_solution(solution: String) -> Result<(), HMError> { 447 | // marginally adapted but mostly stolen from 448 | // https://rust-lang-nursery.github.io/rust-cookbook/os/external.html 449 | 450 | let child: thread::JoinHandle> = thread::spawn(|| { 451 | let output = Command::new("bash") 452 | .arg("-c") 453 | .arg(solution) 454 | .stdout(Stdio::piped()) 455 | .spawn()? 456 | .stdout 457 | .ok_or_else(|| Error::new(ErrorKind::Other, "Couldn't capture stdout"))?; 458 | if cfg!(debug_assertions) { 459 | let reader = BufReader::new(output); 460 | reader 461 | .lines() 462 | .filter_map(|line| line.ok()) 463 | .for_each(|line| println!("{}", line)); 464 | } 465 | Ok(()) 466 | }); 467 | child.join().unwrap() 468 | } 469 | 470 | /// 471 | /// Pretty simple. 472 | /// Hand off to the actual function that does the work. 473 | /// 474 | pub fn perform_operation_on(mo: ManagedObject) -> Result<(), HMError> { 475 | let _s = mo.method.as_str(); 476 | match _s { 477 | "symlink" => { 478 | let source: String = mo.source; 479 | let destination: String = mo.destination; 480 | symlink_item(source, destination, mo.force) 481 | } 482 | "copy" => { 483 | let source: String = mo.source; 484 | let destination: String = mo.destination; 485 | copy_item(source, destination, mo.force) 486 | } 487 | _ => { 488 | println!("{}", style(_s.to_string()).red()); 489 | Ok(()) 490 | } 491 | } 492 | } 493 | 494 | /// 495 | /// Take our list of ManagedObjects to do stuff to, and determine 496 | /// if they're simple or complex (simple is symlink or copy, complex 497 | /// maybe compilation or pulling a git repo). We just do the simple ones, as they 498 | /// won't be computationally expensive. 499 | /// 500 | /// For complex ones we get a list of list of MOs that we can do in some order that 501 | /// satisfies their dependencies, then we hand them off to send_tasks_off_to_college(). 502 | /// 503 | pub fn do_tasks( 504 | a: HashMap, 505 | target_task: Option, 506 | ) -> Result<(), HMError> { 507 | let mut complex_operations = a.clone(); 508 | let mut simple_operations = a; 509 | complex_operations.retain(|_, v| v.is_task()); // all the things that aren't just symlink/copy 510 | simple_operations.retain(|_, v| !v.is_task()); // all the things that are quick (don't need to thread off) 511 | if target_task.is_some() { 512 | let tt_name = target_task.clone().unwrap(); 513 | // only keep the target task, if it's in here... 514 | // we can't do this with complex tasks, because the target may have deps we want 515 | // we'll handle that later in get_task_batches 516 | simple_operations.retain(|_, v| v.name == tt_name); 517 | } 518 | for (_name, _mo) in simple_operations.into_iter() { 519 | // lol postmaclone 520 | let p = _mo.post.clone(); 521 | let a = perform_operation_on(_mo).map_err(|e| { 522 | hmerror::error( 523 | format!("Failed to perform operation on {:#?}", _name).as_str(), 524 | e.to_string().as_str(), 525 | ) 526 | }); 527 | if a.is_ok() { 528 | hmerror::happy_print(format!("Successfully performed operation on {:#?}", _name).as_str()); 529 | if !p.is_empty() { 530 | println!("↳ Executing post {} for {}... ", p, _name); 531 | let _ = execute_solution(p); 532 | } 533 | } 534 | } 535 | let (tx, rx) = mpsc::channel(); 536 | let mp: MultiProgress = MultiProgress::new(); 537 | let mut t: HashSet = HashSet::new(); 538 | let _v = get_task_batches(complex_operations, target_task).unwrap_or_else(|er| { 539 | hmerror::error( 540 | "Error occurred attempting to get task batches", 541 | format!("{}{}", "\n", er.to_string().as_str()).as_str(), 542 | ); 543 | exit(3); 544 | }); 545 | for _a in _v { 546 | for _b in _a { 547 | t.insert(_b.name.to_string()); 548 | let _p: ProgressBar = mp.add(ProgressBar::new_spinner()); 549 | send_tasks_off_to_college(&_b, &tx, _p).expect("ohtehnoes"); 550 | } 551 | } 552 | 553 | let mut v: HashMap = HashMap::new(); 554 | 555 | // create a map of each Worker, and just poll them until they're done 556 | // the hashmap ensures we have only one of each worker 557 | loop { 558 | if let Ok(_t) = rx.try_recv() { 559 | v.insert(_t.name.clone(), _t.clone()); 560 | } 561 | std::thread::sleep(time::Duration::from_millis(10)); 562 | if !all_workers_done(&v) { 563 | continue; 564 | } 565 | break; 566 | } 567 | mp.join().unwrap(); 568 | Ok(()) 569 | } 570 | 571 | /// 572 | /// Iterate through all the workers passed in. If any isn't marked as complete, `return false;`. 573 | /// Let's take a reference, because we're only reading, and don't need ownership. 574 | /// 575 | fn all_workers_done(workers: &HashMap) -> bool { 576 | for w in workers.values() { 577 | if !w.completed { 578 | return false; 579 | } 580 | } 581 | true 582 | } 583 | --------------------------------------------------------------------------------