├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── build.rs ├── cliff.toml ├── dockerfile ├── gtea.toml ├── install.sh ├── lib ├── default.fl ├── init.fl ├── install.fl ├── lib.fl ├── templates │ └── cargo.fl └── utils.fl ├── log ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT └── src │ └── lib.rs ├── saku-cli ├── Cargo.toml └── src │ ├── add.rs │ ├── changelog.rs │ ├── config.rs │ ├── env.rs │ ├── flask.rs │ ├── install.rs │ ├── lib.rs │ ├── list.rs │ ├── remove.rs │ ├── search.rs │ ├── show.rs │ ├── uninstall.rs │ ├── update.rs │ └── upgrade.rs ├── saku-lib ├── Cargo.toml ├── error.rs ├── exec │ ├── cmd.rs │ ├── mod.rs │ ├── pkg.rs │ └── run.rs ├── lib.rs ├── pkg │ ├── config.rs │ ├── data.rs │ ├── flask.rs │ ├── flaskfile.rs │ ├── mod.rs │ ├── pkg.rs │ ├── rebuild.rs │ └── root.rs ├── prelude.rs └── util │ ├── cli.rs │ ├── colors.rs │ ├── constants.rs │ ├── filepath.rs │ ├── io.rs │ ├── mod.rs │ ├── msg.rs │ ├── path.rs │ └── url.rs ├── scripts └── release │ ├── nightly │ ├── changelog.sh │ ├── create.sh │ └── release-name.sh │ └── stable │ ├── changelog.sh │ └── create.sh └── src └── main.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | indent_style = space 2 | indent_size = 2 3 | charset = utf-8 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | branches: [ "mega", "nightly" ] 6 | pull_request: 7 | branches: [ "mega", "nightly" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Build 18 | run: cargo build -vv 19 | - name: Run tests 20 | run: cargo test -p saku-lib -p saku-cli --verbose 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | # Cargo.lock 9 | # saku-lib/Cargo.lock 10 | # saku-cli/Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | 15 | # MSVC Windows builds of rustc generate these, which store debugging information 16 | *.pdb 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.2.0] - 2024-06-10 4 | 5 | ### Bug Fixes 6 | 7 | - (`lib/exec`) Use `fake-tty` crate display colors in command output 8 | - (``) Fix renamed github scope (`comfysage`) 9 | - (`lib/util/filepath`) `get_relative` should not error when trimmed path is unchanged (root is unmatched) 10 | - (`scripts`) Use builtin cliff flags 11 | - (`scripts`) Fix changelog commits 12 | 13 | ### Development 14 | 15 | - (`lib/pkg/root`) Move `is_dir` check out of `link_entry` 16 | - (`lib/pkg/root`) Fix uninstall root fn 17 | - (`lib/pkg/root`) Optimize `read_dir` iterator 18 | 19 | ### Documentation 20 | 21 | - (`readme`) Update install url to use new github raw api 22 | - (`lib/pkg/root`) Add inline docs 23 | - (`changelog`) Update nightly changelog 24 | 25 | ### Features 26 | 27 | - (`cli/uninstall`) Implement uninstall cmd 28 | - (`dockerfile`) Create dockerfile 29 | 30 | ### Miscellaneous Tasks 31 | 32 | - (`gtea`) Add gtea.toml 33 | - (`cargo`) Update `Cargo.lock` 34 | - (``) Add nightly release scripts 35 | - (``) Add stable release scripts 36 | - (``) Update nightly changelog 37 | - (`changelog`) Update nightly changelog 38 | 39 | ### Refactor 40 | 41 | - (`cargo`) Add local libs to workspace 42 | - (`lib/exec`) Refactor commands 43 | 44 | ### Rework 45 | 46 | - (`lib/pkg/root`) Rework root building 47 | - (`lib/pkg/rebuild`) Rework rebuild system 48 | 49 | ### Styling 50 | 51 | - (`cli/install`) Remove redundant note 52 | 53 | 54 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.13" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "utf8parse", 26 | ] 27 | 28 | [[package]] 29 | name = "anstyle" 30 | version = "1.0.6" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 33 | 34 | [[package]] 35 | name = "anstyle-parse" 36 | version = "0.2.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 39 | dependencies = [ 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-query" 45 | version = "1.0.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 48 | dependencies = [ 49 | "windows-sys 0.52.0", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-wincon" 54 | version = "3.0.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 57 | dependencies = [ 58 | "anstyle", 59 | "windows-sys 0.52.0", 60 | ] 61 | 62 | [[package]] 63 | name = "bitflags" 64 | version = "1.3.2" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 67 | 68 | [[package]] 69 | name = "bitflags" 70 | version = "2.4.2" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 73 | 74 | [[package]] 75 | name = "cfg-if" 76 | version = "1.0.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 79 | 80 | [[package]] 81 | name = "clap" 82 | version = "4.5.1" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" 85 | dependencies = [ 86 | "clap_builder", 87 | ] 88 | 89 | [[package]] 90 | name = "clap_builder" 91 | version = "4.5.1" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" 94 | dependencies = [ 95 | "anstream", 96 | "anstyle", 97 | "clap_lex", 98 | "strsim", 99 | ] 100 | 101 | [[package]] 102 | name = "clap_lex" 103 | version = "0.7.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 106 | 107 | [[package]] 108 | name = "colorchoice" 109 | version = "1.0.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 112 | 113 | [[package]] 114 | name = "directories" 115 | version = "5.0.1" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 118 | dependencies = [ 119 | "dirs-sys", 120 | ] 121 | 122 | [[package]] 123 | name = "dirs-sys" 124 | version = "0.4.1" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 127 | dependencies = [ 128 | "libc", 129 | "option-ext", 130 | "redox_users", 131 | "windows-sys 0.48.0", 132 | ] 133 | 134 | [[package]] 135 | name = "env_logger" 136 | version = "0.10.2" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 139 | dependencies = [ 140 | "humantime", 141 | "is-terminal", 142 | "log", 143 | "regex", 144 | "termcolor", 145 | ] 146 | 147 | [[package]] 148 | name = "equivalent" 149 | version = "1.0.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 152 | 153 | [[package]] 154 | name = "fake-tty" 155 | version = "0.3.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "aa6c2a740a5d6940f90a0f13b5828440c2a7160bd1e235cf934d5df0e7a3e1ad" 158 | 159 | [[package]] 160 | name = "getrandom" 161 | version = "0.2.12" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 164 | dependencies = [ 165 | "cfg-if", 166 | "libc", 167 | "wasi", 168 | ] 169 | 170 | [[package]] 171 | name = "glob" 172 | version = "0.3.1" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 175 | 176 | [[package]] 177 | name = "hashbrown" 178 | version = "0.14.3" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 181 | 182 | [[package]] 183 | name = "hermit-abi" 184 | version = "0.3.8" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" 187 | 188 | [[package]] 189 | name = "humantime" 190 | version = "2.1.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 193 | 194 | [[package]] 195 | name = "indexmap" 196 | version = "2.2.3" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" 199 | dependencies = [ 200 | "equivalent", 201 | "hashbrown", 202 | ] 203 | 204 | [[package]] 205 | name = "is-terminal" 206 | version = "0.4.12" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" 209 | dependencies = [ 210 | "hermit-abi", 211 | "libc", 212 | "windows-sys 0.52.0", 213 | ] 214 | 215 | [[package]] 216 | name = "itoa" 217 | version = "1.0.10" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 220 | 221 | [[package]] 222 | name = "lazy_static" 223 | version = "1.4.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 226 | 227 | [[package]] 228 | name = "libc" 229 | version = "0.2.153" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 232 | 233 | [[package]] 234 | name = "libredox" 235 | version = "0.0.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" 238 | dependencies = [ 239 | "bitflags 2.4.2", 240 | "libc", 241 | "redox_syscall", 242 | ] 243 | 244 | [[package]] 245 | name = "log" 246 | version = "0.4.20" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 249 | 250 | [[package]] 251 | name = "memchr" 252 | version = "2.7.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 255 | 256 | [[package]] 257 | name = "minimal-lexical" 258 | version = "0.2.1" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 261 | 262 | [[package]] 263 | name = "nom" 264 | version = "7.1.3" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 267 | dependencies = [ 268 | "memchr", 269 | "minimal-lexical", 270 | ] 271 | 272 | [[package]] 273 | name = "option-ext" 274 | version = "0.2.0" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 277 | 278 | [[package]] 279 | name = "proc-macro2" 280 | version = "1.0.78" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 283 | dependencies = [ 284 | "unicode-ident", 285 | ] 286 | 287 | [[package]] 288 | name = "quote" 289 | version = "1.0.35" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 292 | dependencies = [ 293 | "proc-macro2", 294 | ] 295 | 296 | [[package]] 297 | name = "redox_syscall" 298 | version = "0.4.1" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 301 | dependencies = [ 302 | "bitflags 1.3.2", 303 | ] 304 | 305 | [[package]] 306 | name = "redox_users" 307 | version = "0.4.4" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" 310 | dependencies = [ 311 | "getrandom", 312 | "libredox", 313 | "thiserror", 314 | ] 315 | 316 | [[package]] 317 | name = "regex" 318 | version = "1.10.3" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 321 | dependencies = [ 322 | "aho-corasick", 323 | "memchr", 324 | "regex-automata", 325 | "regex-syntax", 326 | ] 327 | 328 | [[package]] 329 | name = "regex-automata" 330 | version = "0.4.5" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" 333 | dependencies = [ 334 | "aho-corasick", 335 | "memchr", 336 | "regex-syntax", 337 | ] 338 | 339 | [[package]] 340 | name = "regex-syntax" 341 | version = "0.8.2" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 344 | 345 | [[package]] 346 | name = "ryu" 347 | version = "1.0.17" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 350 | 351 | [[package]] 352 | name = "saku" 353 | version = "0.1.0" 354 | dependencies = [ 355 | "clap", 356 | "directories", 357 | "lazy_static", 358 | "nom", 359 | "regex", 360 | "saku-cli", 361 | "saku-lib", 362 | "saku_logger", 363 | "serde", 364 | "serde_yaml", 365 | "toml", 366 | ] 367 | 368 | [[package]] 369 | name = "saku-cli" 370 | version = "0.1.0" 371 | dependencies = [ 372 | "saku-lib", 373 | ] 374 | 375 | [[package]] 376 | name = "saku-lib" 377 | version = "0.1.0" 378 | dependencies = [ 379 | "directories", 380 | "fake-tty", 381 | "glob", 382 | "lazy_static", 383 | "log", 384 | "nom", 385 | "regex", 386 | "serde", 387 | "serde_yaml", 388 | "toml", 389 | ] 390 | 391 | [[package]] 392 | name = "saku_logger" 393 | version = "0.1.0" 394 | dependencies = [ 395 | "env_logger", 396 | "log", 397 | ] 398 | 399 | [[package]] 400 | name = "serde" 401 | version = "1.0.197" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 404 | dependencies = [ 405 | "serde_derive", 406 | ] 407 | 408 | [[package]] 409 | name = "serde_derive" 410 | version = "1.0.197" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 413 | dependencies = [ 414 | "proc-macro2", 415 | "quote", 416 | "syn", 417 | ] 418 | 419 | [[package]] 420 | name = "serde_spanned" 421 | version = "0.6.5" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 424 | dependencies = [ 425 | "serde", 426 | ] 427 | 428 | [[package]] 429 | name = "serde_yaml" 430 | version = "0.9.32" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f" 433 | dependencies = [ 434 | "indexmap", 435 | "itoa", 436 | "ryu", 437 | "serde", 438 | "unsafe-libyaml", 439 | ] 440 | 441 | [[package]] 442 | name = "strsim" 443 | version = "0.11.0" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 446 | 447 | [[package]] 448 | name = "syn" 449 | version = "2.0.51" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" 452 | dependencies = [ 453 | "proc-macro2", 454 | "quote", 455 | "unicode-ident", 456 | ] 457 | 458 | [[package]] 459 | name = "termcolor" 460 | version = "1.4.1" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 463 | dependencies = [ 464 | "winapi-util", 465 | ] 466 | 467 | [[package]] 468 | name = "thiserror" 469 | version = "1.0.57" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" 472 | dependencies = [ 473 | "thiserror-impl", 474 | ] 475 | 476 | [[package]] 477 | name = "thiserror-impl" 478 | version = "1.0.57" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 481 | dependencies = [ 482 | "proc-macro2", 483 | "quote", 484 | "syn", 485 | ] 486 | 487 | [[package]] 488 | name = "toml" 489 | version = "0.8.10" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" 492 | dependencies = [ 493 | "serde", 494 | "serde_spanned", 495 | "toml_datetime", 496 | "toml_edit", 497 | ] 498 | 499 | [[package]] 500 | name = "toml_datetime" 501 | version = "0.6.5" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 504 | dependencies = [ 505 | "serde", 506 | ] 507 | 508 | [[package]] 509 | name = "toml_edit" 510 | version = "0.22.6" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" 513 | dependencies = [ 514 | "indexmap", 515 | "serde", 516 | "serde_spanned", 517 | "toml_datetime", 518 | "winnow", 519 | ] 520 | 521 | [[package]] 522 | name = "unicode-ident" 523 | version = "1.0.12" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 526 | 527 | [[package]] 528 | name = "unsafe-libyaml" 529 | version = "0.2.10" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" 532 | 533 | [[package]] 534 | name = "utf8parse" 535 | version = "0.2.1" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 538 | 539 | [[package]] 540 | name = "wasi" 541 | version = "0.11.0+wasi-snapshot-preview1" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 544 | 545 | [[package]] 546 | name = "winapi" 547 | version = "0.3.9" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 550 | dependencies = [ 551 | "winapi-i686-pc-windows-gnu", 552 | "winapi-x86_64-pc-windows-gnu", 553 | ] 554 | 555 | [[package]] 556 | name = "winapi-i686-pc-windows-gnu" 557 | version = "0.4.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 560 | 561 | [[package]] 562 | name = "winapi-util" 563 | version = "0.1.6" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 566 | dependencies = [ 567 | "winapi", 568 | ] 569 | 570 | [[package]] 571 | name = "winapi-x86_64-pc-windows-gnu" 572 | version = "0.4.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 575 | 576 | [[package]] 577 | name = "windows-sys" 578 | version = "0.48.0" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 581 | dependencies = [ 582 | "windows-targets 0.48.5", 583 | ] 584 | 585 | [[package]] 586 | name = "windows-sys" 587 | version = "0.52.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 590 | dependencies = [ 591 | "windows-targets 0.52.3", 592 | ] 593 | 594 | [[package]] 595 | name = "windows-targets" 596 | version = "0.48.5" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 599 | dependencies = [ 600 | "windows_aarch64_gnullvm 0.48.5", 601 | "windows_aarch64_msvc 0.48.5", 602 | "windows_i686_gnu 0.48.5", 603 | "windows_i686_msvc 0.48.5", 604 | "windows_x86_64_gnu 0.48.5", 605 | "windows_x86_64_gnullvm 0.48.5", 606 | "windows_x86_64_msvc 0.48.5", 607 | ] 608 | 609 | [[package]] 610 | name = "windows-targets" 611 | version = "0.52.3" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" 614 | dependencies = [ 615 | "windows_aarch64_gnullvm 0.52.3", 616 | "windows_aarch64_msvc 0.52.3", 617 | "windows_i686_gnu 0.52.3", 618 | "windows_i686_msvc 0.52.3", 619 | "windows_x86_64_gnu 0.52.3", 620 | "windows_x86_64_gnullvm 0.52.3", 621 | "windows_x86_64_msvc 0.52.3", 622 | ] 623 | 624 | [[package]] 625 | name = "windows_aarch64_gnullvm" 626 | version = "0.48.5" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 629 | 630 | [[package]] 631 | name = "windows_aarch64_gnullvm" 632 | version = "0.52.3" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" 635 | 636 | [[package]] 637 | name = "windows_aarch64_msvc" 638 | version = "0.48.5" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 641 | 642 | [[package]] 643 | name = "windows_aarch64_msvc" 644 | version = "0.52.3" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" 647 | 648 | [[package]] 649 | name = "windows_i686_gnu" 650 | version = "0.48.5" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 653 | 654 | [[package]] 655 | name = "windows_i686_gnu" 656 | version = "0.52.3" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" 659 | 660 | [[package]] 661 | name = "windows_i686_msvc" 662 | version = "0.48.5" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 665 | 666 | [[package]] 667 | name = "windows_i686_msvc" 668 | version = "0.52.3" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" 671 | 672 | [[package]] 673 | name = "windows_x86_64_gnu" 674 | version = "0.48.5" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 677 | 678 | [[package]] 679 | name = "windows_x86_64_gnu" 680 | version = "0.52.3" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" 683 | 684 | [[package]] 685 | name = "windows_x86_64_gnullvm" 686 | version = "0.48.5" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 689 | 690 | [[package]] 691 | name = "windows_x86_64_gnullvm" 692 | version = "0.52.3" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" 695 | 696 | [[package]] 697 | name = "windows_x86_64_msvc" 698 | version = "0.48.5" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 701 | 702 | [[package]] 703 | name = "windows_x86_64_msvc" 704 | version = "0.52.3" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" 707 | 708 | [[package]] 709 | name = "winnow" 710 | version = "0.6.2" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178" 713 | dependencies = [ 714 | "memchr", 715 | ] 716 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | workspace = { members = [ "saku-lib", "saku-cli", "log", ] } 2 | 3 | [package] 4 | name = "saku" 5 | version = "0.1.0" 6 | edition = "2021" 7 | readme = "README.md" 8 | repository = "https://github.com/comfysage/saku" 9 | license = "MIT OR Apache-2.0" 10 | 11 | [[bin]] 12 | name = "sk" 13 | path = "src/main.rs" 14 | 15 | [dependencies] 16 | clap = { version = "4.4.4", features = ["cargo", "default"] } 17 | saku-lib = { path = "./saku-lib" } 18 | saku-cli = { path = "./saku-cli" } 19 | saku_logger = { path = "./log" } 20 | directories = "5.0.1" 21 | lazy_static = "1.4.0" 22 | regex = "1.9.6" 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_yaml = "0.9" 25 | toml = "0.8.2" 26 | nom = "7.1.3" 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # saku 2 | 3 | :seedling: a tiny distro-independent package manager written in Rust. 4 | 5 | ![saku-show](./assets/show-neovim.gif) 6 | 7 | # Saku 8 | 9 | Saku is a lightweight package manager for macOS/linux, written in Rust. It aims to 10 | provide an alternative solution to managing software packages on macOS/linux systems, 11 | addressing some of the issues faced with existing package managers like 12 | Homebrew. 13 | 14 | ## Features 15 | 16 | - Simplified package management: Saku strives to provide a streamlined and 17 | user-friendly package management experience, making it easy to install, 18 | update, and remove software packages. 19 | 20 | - Lightweight and efficient: Saku is designed to be lightweight and 21 | efficient, minimizing resource usage while maintaining performance. It aims 22 | to provide fast and responsive package management operations. 23 | 24 | - Improved stability: Saku focuses on stability and reliability, aiming to 25 | minimize dependency conflicts and provide a robust package installation 26 | process. 27 | 28 | - Self maintaining: Saku has built-in support for installing updates and 29 | configuring new packages. 30 | 31 | ## Installation 32 | 33 | Run the installer using curl: 34 | 35 | ```shell 36 | curl -fsSL https://github.com/comfysage/saku/raw/mega/install.sh | sh 37 | ``` 38 | 39 | Setup saku environment, add this to your `.bashrc`/`.zshrc`: 40 | 41 | ```shell 42 | eval "$($HOME/.saku/root/bin/saku env)" 43 | ``` 44 | 45 | ## Usage 46 | 47 | Saku provides a simple command-line interface for managing packages. Here 48 | are some common commands: 49 | 50 | - `saku install `: Installs the specified package. 51 | - `saku update`: Update sources. 52 | - `saku upgrade `: Updates the specified package to the latest version. 53 | - `saku remove `: Removes the specified package. 54 | 55 | For more information on available commands and options, refer to the Saku 56 | documentation. 57 | 58 | ## Contributing 59 | 60 | Contributions to Saku are welcome! If you encounter any issues, have 61 | suggestions for improvements, or want to contribute new features, please submit 62 | a pull request or open an issue on the GitHub repository. 63 | 64 | Before contributing, please review the [contribution 65 | guidelines](https://github.com/comfysage/saku/blob/main/CONTRIBUTING.md) 66 | for instructions on how to contribute code, report bugs, and more. 67 | 68 | ## License 69 | 70 | Saku is released under the MIT License. Please review the license file for more details. 71 | 72 | ## Acknowledgements 73 | 74 | Saku is inspired by package managers like Homebrew. Special thanks to the 75 | contributors and the open-source community for their valuable contributions. 76 | 77 | ## Contact 78 | 79 | For any inquiries or questions, feel free to contact the project maintainer at 80 | [[67917529+comfysage@users.noreply.github.com]]. 81 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::process::{Command, Stdio}; 3 | 4 | fn capture_command(command: &mut Command) -> String { 5 | let command = command 6 | .stdout(Stdio::piped()) // Capture stdout 7 | .spawn(); 8 | 9 | // Check if the command was successfully created 10 | let mut child = match command { 11 | Ok(child) => child, 12 | Err(e) => { 13 | eprintln!("Failed to execute command: {}", e); 14 | std::process::exit(1); 15 | } 16 | }; 17 | 18 | // Read the captured stdout 19 | let mut output = String::new(); 20 | child 21 | .stdout 22 | .take() 23 | .unwrap() 24 | .read_to_string(&mut output) 25 | .unwrap(); 26 | 27 | // Wait for the command to finish and check its exit status 28 | let status = child.wait().expect("Failed to wait for child process"); 29 | if status.success() { 30 | println!("Command executed successfully. Output:\n{}", output); 31 | } else { 32 | eprintln!("Command failed with exit code: {}", status); 33 | } 34 | 35 | output 36 | } 37 | 38 | struct Vars { 39 | repo_branch: String, 40 | commit_hash: String, 41 | } 42 | 43 | impl Vars { 44 | pub fn new() -> Self { 45 | let mut repo_branch_bind = Command::new("git"); 46 | let repo_branch_cmd = repo_branch_bind.arg("branch").arg("--show-current"); 47 | let mut commit_hash_bind = Command::new("git"); 48 | let commit_hash_cmd = commit_hash_bind.arg("rev-parse").arg("HEAD"); 49 | let mut repo_branch = capture_command(repo_branch_cmd); 50 | repo_branch = repo_branch.replace('\n', ""); 51 | let mut commit_hash = capture_command(commit_hash_cmd); 52 | commit_hash = commit_hash.chars().take(7).collect(); 53 | 54 | Self { 55 | repo_branch, 56 | commit_hash, 57 | } 58 | } 59 | } 60 | 61 | fn main() { 62 | let vars = Vars::new(); 63 | println!("cargo:rustc-env=REPO_BRANCH={}", vars.repo_branch); 64 | println!("cargo:rustc-env=COMMIT_HASH={}", vars.commit_hash); 65 | let mut pkg_version = format!("{}-{}", vars.repo_branch, vars.commit_hash); 66 | if std::env::var("PROFILE").unwrap() == "debug" { 67 | pkg_version = format!("{pkg_version}-debug"); 68 | } 69 | println!("cargo:rustc-env=PKG_VERSION={}", pkg_version); 70 | } 71 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | [changelog] 2 | # changelog header 3 | header = """ 4 | # Changelog\n 5 | """ 6 | # template for the changelog body 7 | # https://keats.github.io/tera/docs/#introduction 8 | body = """ 9 | {% if version %}\ 10 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 11 | {% else %}\ 12 | ## [unreleased] 13 | {% endif %}\ 14 | {% for group, commits in commits | group_by(attribute="group") %} 15 | ### {{ group | upper_first }} 16 | {% for commit in commits %} 17 | - {% if commit.breaking %}[**breaking**] {% endif %} (`{{ commit.scope }}`) {{ commit.message | upper_first }}\ 18 | {% endfor %} 19 | {% endfor %}\n 20 | """ 21 | # remove the leading and trailing whitespace from the template 22 | trim = true 23 | # changelog footer 24 | footer = """ 25 | 26 | """ 27 | # postprocessors 28 | postprocessors = [ 29 | { pattern = '', replace = "https://github.com/comfysage/saku" }, # replace repository URL 30 | ] 31 | [git] 32 | # parse the commits based on https://www.conventionalcommits.org 33 | conventional_commits = true 34 | # filter out the commits that are not conventional 35 | filter_unconventional = true 36 | # process each line of a commit as an individual commit 37 | split_commits = false 38 | # regex for preprocessing the commit messages 39 | commit_preprocessors = [ 40 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, # replace issue numbers 41 | ] 42 | # regex for parsing and grouping commits 43 | commit_parsers = [ 44 | { message = "^rework", group = "Rework" }, 45 | { message = "^feat", group = "Features" }, 46 | { message = "^add", group = "Development" }, 47 | { message = "^fix", group = "Bug Fixes" }, 48 | { message = "^doc", group = "Documentation" }, 49 | { message = "^perf", group = "Performance" }, 50 | { message = "^refactor", group = "Refactor" }, 51 | { message = "^style", group = "Styling" }, 52 | { message = "^test", group = "Testing" }, 53 | { message = "^chore\\(release\\): prepare for", skip = true }, 54 | { message = "^chore\\(deps\\)", skip = true }, 55 | { message = "^chore\\(pr\\)", skip = true }, 56 | { message = "^chore\\(pull\\)", skip = true }, 57 | { message = "^chore", group = "Miscellaneous Tasks" }, 58 | { message = "^workflow|ci", group = "CI" }, 59 | { body = ".*security", group = "Security" }, 60 | { message = "^revert", group = "Revert" }, 61 | ] 62 | # protect breaking changes from being skipped due to matching a skipping commit_parser 63 | protect_breaking_commits = false 64 | # filter out the commits that are not matched by commit parsers 65 | filter_commits = false 66 | # glob pattern for matching git tags 67 | tag_pattern = "v[0-9]*" 68 | # regex for skipping tags 69 | skip_tags = "v0.1.0-beta.1" 70 | # regex for ignoring tags 71 | ignore_tags = "" 72 | # sort the tags topologically 73 | topo_order = false 74 | # sort the commits inside sections by oldest/newest order 75 | sort_commits = "oldest" 76 | # limit the number of commits included in the changelog. 77 | # limit_commits = 42 78 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine as builder 2 | 3 | RUN apk update 4 | 5 | # █▀█ █░█ █▀ ▀█▀ 6 | # █▀▄ █▄█ ▄█ ░█░ 7 | 8 | RUN apk add rustup &&\ 9 | rustup-init -v -y --no-modify-path --profile minimal 10 | ENV PATH="${PATH}:/root/.cargo/bin" 11 | 12 | # █▀▄ █▀▀ █▀█ █▀▀ █▄░█ █▀▄ █▀▀ █▄░█ █▀▀ █ █▀▀ █▀ 13 | # █▄▀ ██▄ █▀▀ ██▄ █░▀█ █▄▀ ██▄ █░▀█ █▄▄ █ ██▄ ▄█ 14 | 15 | RUN apk add --no-interactive git bash curl clang 16 | 17 | # █ █▄░█ █▀ ▀█▀ ▄▀█ █░░ █░░ █▀▀ █▀█ 18 | # █ █░▀█ ▄█ ░█░ █▀█ █▄▄ █▄▄ ██▄ █▀▄ 19 | 20 | COPY ./install.sh . 21 | 22 | RUN chmod u+x ./install.sh && ./install.sh 23 | 24 | SHELL ["bash"] 25 | CMD ["bash"] 26 | -------------------------------------------------------------------------------- /gtea.toml: -------------------------------------------------------------------------------- 1 | [main] 2 | branch = "mega" 3 | 4 | [nightly] 5 | branch = "nightly" 6 | enable = true 7 | 8 | [feature] 9 | prefix = "feat" 10 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SAKUPATH="${SAKUPATH:-$HOME/.saku}" 4 | 5 | set -e 6 | 7 | printf " -\033[35m setting up\033[0m saku root\n" 8 | mkdir -p $SAKUPATH/repo 9 | cd $SAKUPATH/repo 10 | 11 | printf " -\033[35m cloning\033[0m saku from\033[33m https://github.com/comfysage/saku\033[0m\n" 12 | git clone --filter=blob:none https://github.com/comfysage/saku && cd saku 13 | 14 | printf " -\033[35m building\033[0m saku\n" 15 | cargo build -r 16 | 17 | printf " -\033[35m setting up\033[0m environment\n" 18 | ./target/release/sk config init 19 | 20 | printf " -\033[35m finishing\033[0m installation\n" 21 | ./target/release/sk task install saku 22 | 23 | -------------------------------------------------------------------------------- /lib/default.fl: -------------------------------------------------------------------------------- 1 | name="$(basename $2)" 2 | sakudir="${SAKUPATH:-"$HOME/.saku"}" 3 | flaskfile="$sakudir/flask/$1/$2.fl" 4 | srcdir="$sakudir/repo/$name" 5 | store="$sakudir/store" 6 | storedir="$store/$name" 7 | 8 | pkgname="" 9 | pkgdesc="" 10 | url="" 11 | depends=() 12 | makedepends=() 13 | provides=() 14 | 15 | prepare() { 16 | trace "preparing $pkgname" 17 | } 18 | 19 | build() { 20 | trace "building $pkgname" 21 | } 22 | 23 | package() { 24 | trace "packaging $pkgname" 25 | } 26 | -------------------------------------------------------------------------------- /lib/init.fl: -------------------------------------------------------------------------------- 1 | # usage 2 | # . path/to/init.fl pkggroup pkgname && _install 3 | 4 | set -e 5 | 6 | # -- init -- 7 | 8 | DIR=$(pwd) 9 | 10 | if [[ $(basename $SHELL) -eq "bash" ]]; then 11 | SOURCE=${BASH_SOURCE[0]} 12 | 13 | # resolve $SOURCE until the file is no longer a symlink 14 | while [ -L "$SOURCE" ]; do 15 | DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd ) 16 | SOURCE=$(readlink "$SOURCE") 17 | 18 | # if $SOURCE was a relative symlink, we need to resolve it relative to the 19 | # path where the symlink file was located 20 | [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE 21 | done 22 | 23 | DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd ) 24 | elif [[ $(basename $SHELL) -eq "zsh" ]]; then 25 | DIR=$( dirname "$0" ) 26 | fi 27 | 28 | # -- load lib -- 29 | 30 | . "$DIR/lib.fl" 31 | 32 | # -- load pkg -- 33 | 34 | loadpkgfile "$1" "$2" 35 | 36 | # -- load fn -- 37 | 38 | loadtemplates 39 | 40 | _build() { 41 | trace "preparing installation" 42 | prepare 43 | trace "building $pkgname" 44 | build 45 | if ! [[ -z "$template" ]]; then 46 | for tmpl in ${template[@]}; do 47 | has_fn build_$tmpl && build_$tmpl 48 | done 49 | fi 50 | } 51 | 52 | _install() { 53 | trace "installing $pkgname" 54 | package 55 | if ! [[ -z "$template" ]]; then 56 | for tmpl in ${template[@]}; do 57 | has_fn package_$tmpl && package_$tmpl 58 | done 59 | fi 60 | } 61 | 62 | _upgrade() { 63 | _build 64 | has_fn preupdate && preupdate 65 | } 66 | 67 | _cleanup() { 68 | has_fn preupdate && preupdate 69 | has_fn cleanup && cleanup 70 | if ! [[ -z "$template" ]]; then 71 | for tmpl in ${template[@]}; do 72 | has_fn cleanup_$tmpl && cleanup_$tmpl 73 | done 74 | fi 75 | } 76 | 77 | # vim: ft=bash 78 | -------------------------------------------------------------------------------- /lib/install.fl: -------------------------------------------------------------------------------- 1 | store() { 2 | src="$srcdir/$1" 3 | dst="$storedir/$2" 4 | 5 | [[ -f "$src" || -d "$src" ]] || die "src does not exist [$src]" 6 | 7 | mkdir -p "$(dirname $dst)" 8 | 9 | echo "$src --> $dst" 10 | perms=0644 11 | [[ -x "$src" ]] && perms=0755 12 | if [[ -f "$src" ]]; then 13 | install -Dm $perms "$src" "$dst" 14 | elif [[ -d "$src" ]]; then 15 | cp -r "$src" "$dst" 16 | fi 17 | } 18 | 19 | # vim: ft=bash 20 | -------------------------------------------------------------------------------- /lib/lib.fl: -------------------------------------------------------------------------------- 1 | . "$DIR/utils.fl" 2 | 3 | loadpkgfile() { 4 | . "$DIR/default.fl" "$1" "$2" 5 | 6 | cd $srcdir || die "$srcdir does not exist" 7 | debug "srcdir [ $srcdir ] " 8 | 9 | file_exists $flaskfile 10 | debug "pkg [ $flaskfile ] " 11 | . "$flaskfile" 12 | 13 | . "$DIR/install.fl" 14 | } 15 | 16 | loadtemplates() { 17 | if ! [[ -z "$template" ]]; then 18 | for tmpl in ${template[@]}; do 19 | . "$DIR/templates/$tmpl.fl" && debug "adding template $tmpl" || die "template '$tmpl' does not exist" 20 | done 21 | fi 22 | } 23 | -------------------------------------------------------------------------------- /lib/templates/cargo.fl: -------------------------------------------------------------------------------- 1 | build_cargo() { 2 | cargo build -r 3 | } 4 | 5 | package_cargo() { 6 | for file in ${bin[@]}; do 7 | store "target/release/$file" "bin/$file" 8 | done 9 | } 10 | 11 | cleanup_cargo() { 12 | rm target -rf 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils.fl: -------------------------------------------------------------------------------- 1 | # -- utils -- 2 | 3 | trace() { 4 | [[ "$RUST_LOG" == "trace" ]] && printf "\033[32;1m%s\033[m %s\n" " --$PROMPT" "$*" || return 0 5 | } 6 | 7 | debug() { 8 | [[ "$RUST_LOG" == "trace" || "$RUST_LOG" == "debug" ]] && printf "\033[32;1m%s\033[m %s\n" " $PROMPT" "$*" || return 0 9 | } 10 | 11 | msg_line() { 12 | printf "\033[32;1m%s\033[m %s\n" " --$PROMPT" "$*" 13 | } 14 | 15 | msg() { 16 | printf "\033[32;1m%s\033[m %s\n" " $PROMPT" "$*" 17 | } 18 | 19 | warn() { 20 | >&2 printf "\033[33;1m%s \033[mwarning: %s\n" "$PROMPT" "$*" 21 | } 22 | 23 | die() { 24 | >&2 printf "\033[31;1m%s \033[merror: %s\n" "$PROMPT" "$*" 25 | exit 1 26 | } 27 | 28 | confirm() { 29 | >&2 printf "\033[33;1m%s \033[mconfirm? %s" "$PROMPT" "$CONFIRM_PROMPT" 30 | read -r ans 31 | if [ "$ans" != y ] ; then 32 | >&2 printf '%s\n' 'Exiting.' 33 | exit 34 | fi 35 | } 36 | 37 | # This is just a simple wrapper around 'command -v' to avoid 38 | # spamming '>/dev/null' throughout this function. This also guards 39 | # against aliases and functions. 40 | has() { 41 | _cmd=$(command -v "$1") 2>/dev/null || return 1 42 | [ -x "$_cmd" ] || return 1 43 | } 44 | 45 | has_fn() { 46 | _cmd=$(type -t $1) 2>/dev/null || return 1 47 | [ -z "$_cmd" ] && return 1 || return 0 48 | } 49 | 50 | file_exists() { 51 | file="$1" 52 | [[ -f "$file" ]] || die "file does not exist [$file]." 53 | } 54 | 55 | # vim: ft=bash 56 | -------------------------------------------------------------------------------- /log/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "saku_logger" 3 | version = "0.1.0" # don't forget to update html_root_url 4 | description = "a visually pretty env_logger for saku" 5 | license = "MIT/Apache-2.0" 6 | categories = ["development-tools::debugging"] 7 | keywords = ["log", "logger", "logging"] 8 | 9 | include = [ 10 | "Cargo.toml", 11 | "LICENSE-APACHE", 12 | "LICENSE-MIT", 13 | "src/**/*" 14 | ] 15 | 16 | [dependencies] 17 | env_logger = "0.10" 18 | log = "0.4" 19 | -------------------------------------------------------------------------------- /log/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /log/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Sean McArthur 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /log/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(test, deny(warnings))] 2 | #![deny(missing_docs)] 3 | #![doc(html_root_url = "https://docs.rs/saku_logger/0.1.0")] 4 | 5 | //! A logger configured via an environment variable which writes to standard 6 | //! error with nice colored output for log levels. 7 | //! 8 | //! ## Example 9 | //! 10 | //! ``` 11 | //! extern crate saku_logger; 12 | //! #[macro_use] extern crate log; 13 | //! 14 | //! fn main() { 15 | //! saku_logger::init(); 16 | //! 17 | //! trace!("a trace example"); 18 | //! debug!("deboogging"); 19 | //! info!("such information"); 20 | //! warn!("o_O"); 21 | //! error!("boom"); 22 | //! } 23 | //! ``` 24 | //! 25 | //! Run the program with the environment variable `RUST_LOG=trace`. 26 | //! 27 | //! ## Defaults 28 | //! 29 | //! The defaults can be setup by calling `init()` or `try_init()` at the start 30 | //! of the program. 31 | //! 32 | //! ## Enable logging 33 | //! 34 | //! This crate uses [env_logger][] internally, so the same ways of enabling 35 | //! logs through an environment variable are supported. 36 | //! 37 | //! ## Changes 38 | //! 39 | //! changes from [pretty_env_logger](https://docs.rs/pretty_env_logger/0.5.0/pretty_env_logger/): 40 | //! - the `RUST_LOG` environment variable defaults to `info`. 41 | //! 42 | //! [env_logger]: https://docs.rs/env_logger 43 | 44 | #[doc(hidden)] 45 | pub extern crate env_logger; 46 | 47 | extern crate log; 48 | 49 | use std::fmt; 50 | use std::sync::atomic::{AtomicUsize, Ordering}; 51 | 52 | use env_logger::{ 53 | fmt::{Color, Style, StyledValue}, 54 | Builder, 55 | }; 56 | use log::Level; 57 | 58 | /// Initializes the global logger with a pretty env logger. 59 | /// 60 | /// This should be called early in the execution of a Rust program, and the 61 | /// global logger may only be initialized once. Future initialization attempts 62 | /// will return an error. 63 | /// 64 | /// # Panics 65 | /// 66 | /// This function fails to set the global logger if one has already been set. 67 | pub fn init() { 68 | try_init().unwrap(); 69 | } 70 | 71 | /// Initializes the global logger with a timed pretty env logger. 72 | /// 73 | /// This should be called early in the execution of a Rust program, and the 74 | /// global logger may only be initialized once. Future initialization attempts 75 | /// will return an error. 76 | /// 77 | /// # Panics 78 | /// 79 | /// This function fails to set the global logger if one has already been set. 80 | pub fn init_timed() { 81 | try_init_timed().unwrap(); 82 | } 83 | 84 | /// Initializes the global logger with a pretty env logger. 85 | /// 86 | /// This should be called early in the execution of a Rust program, and the 87 | /// global logger may only be initialized once. Future initialization attempts 88 | /// will return an error. 89 | /// 90 | /// # Errors 91 | /// 92 | /// This function fails to set the global logger if one has already been set. 93 | pub fn try_init() -> Result<(), log::SetLoggerError> { 94 | try_init_custom_env("RUST_LOG") 95 | } 96 | 97 | /// Initializes the global logger with a timed pretty env logger. 98 | /// 99 | /// This should be called early in the execution of a Rust program, and the 100 | /// global logger may only be initialized once. Future initialization attempts 101 | /// will return an error. 102 | /// 103 | /// # Errors 104 | /// 105 | /// This function fails to set the global logger if one has already been set. 106 | pub fn try_init_timed() -> Result<(), log::SetLoggerError> { 107 | try_init_timed_custom_env("RUST_LOG") 108 | } 109 | 110 | /// Initialized the global logger with a pretty env logger, with a custom variable name. 111 | /// 112 | /// This should be called early in the execution of a Rust program, and the 113 | /// global logger may only be initialized once. Future initialization attempts 114 | /// will return an error. 115 | /// 116 | /// # Panics 117 | /// 118 | /// This function fails to set the global logger if one has already been set. 119 | pub fn init_custom_env(environment_variable_name: &str) { 120 | try_init_custom_env(environment_variable_name).unwrap(); 121 | } 122 | 123 | /// Initialized the global logger with a pretty env logger, with a custom variable name. 124 | /// 125 | /// This should be called early in the execution of a Rust program, and the 126 | /// global logger may only be initialized once. Future initialization attempts 127 | /// will return an error. 128 | /// 129 | /// # Errors 130 | /// 131 | /// This function fails to set the global logger if one has already been set. 132 | pub fn try_init_custom_env(environment_variable_name: &str) -> Result<(), log::SetLoggerError> { 133 | let mut builder = formatted_builder(); 134 | 135 | let env = ::std::env::var(environment_variable_name).unwrap_or("info".to_string()); 136 | builder.parse_filters(&env); 137 | 138 | builder.try_init() 139 | } 140 | 141 | /// Initialized the global logger with a timed pretty env logger, with a custom variable name. 142 | /// 143 | /// This should be called early in the execution of a Rust program, and the 144 | /// global logger may only be initialized once. Future initialization attempts 145 | /// will return an error. 146 | /// 147 | /// # Errors 148 | /// 149 | /// This function fails to set the global logger if one has already been set. 150 | pub fn try_init_timed_custom_env( 151 | environment_variable_name: &str, 152 | ) -> Result<(), log::SetLoggerError> { 153 | let mut builder = formatted_timed_builder(); 154 | 155 | let env = ::std::env::var(environment_variable_name).unwrap_or("info".to_string()); 156 | builder.parse_filters(&env); 157 | 158 | builder.try_init() 159 | } 160 | 161 | /// Returns a `env_logger::Builder` for further customization. 162 | /// 163 | /// This method will return a colored and formatted `env_logger::Builder` 164 | /// for further customization. Refer to env_logger::Build crate documentation 165 | /// for further details and usage. 166 | pub fn formatted_builder() -> Builder { 167 | let mut builder = Builder::new(); 168 | 169 | builder.format(|f, record| { 170 | use std::io::Write; 171 | 172 | let target = record.target(); 173 | let max_width = max_target_width(target); 174 | 175 | let mut style = f.style(); 176 | let level = colored_level(&mut style, record.level()); 177 | 178 | let mut style = f.style(); 179 | let target = style.set_bold(true).value(Padded { 180 | value: target, 181 | width: max_width, 182 | }); 183 | 184 | match record.level() { 185 | Level::Info => writeln!(f, " {} {}", level, record.args(),), 186 | _ => writeln!(f, " {} {} {}", level, target, record.args(),), 187 | } 188 | }); 189 | 190 | builder 191 | } 192 | 193 | /// Returns a `env_logger::Builder` for further customization. 194 | /// 195 | /// This method will return a colored and time formatted `env_logger::Builder` 196 | /// for further customization. Refer to env_logger::Build crate documentation 197 | /// for further details and usage. 198 | pub fn formatted_timed_builder() -> Builder { 199 | let mut builder = Builder::new(); 200 | 201 | builder.format(|f, record| { 202 | use std::io::Write; 203 | let target = record.target(); 204 | let max_width = max_target_width(target); 205 | 206 | let mut style = f.style(); 207 | let level = colored_level(&mut style, record.level()); 208 | 209 | let mut style = f.style(); 210 | let target = style.set_bold(true).value(Padded { 211 | value: target, 212 | width: max_width, 213 | }); 214 | 215 | let time = f.timestamp_millis(); 216 | 217 | match record.level() { 218 | Level::Info => writeln!(f, " {} {} {}", time, level, record.args(),), 219 | _ => writeln!(f, " {} {} {} {}", time, level, target, record.args(),), 220 | } 221 | }); 222 | 223 | builder 224 | } 225 | 226 | struct Padded { 227 | value: T, 228 | width: usize, 229 | } 230 | 231 | impl fmt::Display for Padded { 232 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 233 | write!(f, "{: usize { 240 | let max_width = MAX_MODULE_WIDTH.load(Ordering::Relaxed); 241 | if max_width < target.len() { 242 | MAX_MODULE_WIDTH.store(target.len(), Ordering::Relaxed); 243 | target.len() 244 | } else { 245 | max_width 246 | } 247 | } 248 | 249 | fn colored_level<'a>(style: &'a mut Style, level: Level) -> StyledValue<'a, &'static str> { 250 | match level { 251 | Level::Trace => style.set_color(Color::Magenta).value("TRACE"), 252 | Level::Debug => style.set_color(Color::Ansi256(15)).value("DEBUG"), 253 | Level::Info => style.set_color(Color::Green).value("INFO "), 254 | Level::Warn => style.set_color(Color::Yellow).value("WARN "), 255 | Level::Error => style.set_color(Color::Red).value("ERROR"), 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /saku-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "saku-cli" 3 | version = "0.1.0" 4 | 5 | [lib] 6 | path = "src/lib.rs" 7 | 8 | [dependencies] 9 | saku-lib = { path = "../saku-lib" } 10 | -------------------------------------------------------------------------------- /saku-cli/src/add.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::pkg::data::{get_pkg_from_path, save_pkg}; 2 | use saku_lib::pkg::pkg::Pkg; 3 | 4 | use saku_lib::util::{msg, path}; 5 | 6 | use saku_lib::prelude::*; 7 | 8 | pub fn add(pkg: &Pkg) -> Result<()> { 9 | if path::pkg_exists("custom", &pkg.name) { 10 | return Err(make_err!(Conflict, "pkg already exists.")); 11 | } 12 | 13 | save_pkg(&pkg)?; 14 | 15 | Ok(()) 16 | } 17 | 18 | // add new pkg from file 19 | pub fn add_local(path: &str) -> Result<()> { 20 | msg::add(path); 21 | 22 | let pkg = get_pkg_from_path(path)?; 23 | add(&pkg)?; 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /saku-cli/src/changelog.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::prelude::*; 2 | 3 | use saku_lib::exec; 4 | use saku_lib::util::msg; 5 | use saku_lib::util::path; 6 | 7 | pub fn changelog(name: &str) -> Result<()> { 8 | msg::changelog(name, &path::repo(name)); 9 | 10 | exec::Cmd::Log{ name: name.to_string() }.exec()?; 11 | 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /saku-cli/src/config.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::exec; 2 | use saku_lib::pkg; 3 | use saku_lib::pkg::config::Config; 4 | use saku_lib::prelude::*; 5 | use saku_lib::util::filepath; 6 | use saku_lib::util::{constants, io, msg, path}; 7 | 8 | use crate::{flask, update}; 9 | 10 | fn create_root() -> Result<()> { 11 | io::mkdir(constants::ROOT_DIR.to_string())?; 12 | io::mkdir(path::root_dir("man"))?; 13 | io::mkdir(path::root_dir("share"))?; 14 | 15 | if !path::exists(&path::root_dir("share/man")) { 16 | exec::Cmd::Link { target: "../man".to_string(), path: path::root_dir("share/man") }.exec()?; 17 | } 18 | Ok(()) 19 | } 20 | 21 | pub fn init() -> Result<()> { 22 | io::mkdir(constants::SAKU_DIR.to_string())?; 23 | io::mkdir(constants::PKG_DIR.to_string())?; 24 | io::mkdir(constants::REPO_DIR.to_string())?; 25 | 26 | io::mkdir(constants::FLASK_DIR.to_string())?; 27 | 28 | if !filepath::exists(&*constants::LIB_DIR) { 29 | let repo_dir: String = path::repo("saku"); 30 | let lib_dir: String = constants::LIB_DIR_NAME.to_string(); 31 | let target = filepath::join(&repo_dir, &lib_dir); 32 | io::link(&target, &*constants::LIB_DIR)?; 33 | } 34 | 35 | create_root()?; 36 | 37 | io::mkdir(path::gr("custom"))?; 38 | 39 | if !path::pkg_exists("flasks", "core") { 40 | msg::fetch("core", "https://github.com/comfysage/pkg"); 41 | 42 | flask::add_with_name("core", "comfysage/pkg")?; 43 | } 44 | 45 | if !path::repo_exists("core") { 46 | update::update()?; 47 | } 48 | 49 | Ok(()) 50 | } 51 | 52 | pub fn create() -> Result<()> { 53 | let config_path = Config::path()?; 54 | if path::exists(&config_path) { 55 | return Err(make_err!( 56 | Conflict, 57 | "config file {config_path} already exists." 58 | )); 59 | } 60 | msg::create_config(&config_path); 61 | pkg::data::save_config(Config::default())?; 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /saku-cli/src/env.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::util::path; 2 | use saku_lib::prelude::*; 3 | 4 | pub fn env() -> Result<()> { 5 | let script = format!( 6 | "\ 7 | export PATH=\"$PATH:{}\" 8 | export XDG_DATA_DIRS=\"${{XDG_DATA_DIRS:-/usr/share:/usr/local/share}}:{}\"", 9 | path::root_dir("bin"), 10 | path::root_dir("share") 11 | ); 12 | println!("{script}"); 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /saku-cli/src/flask.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::pkg::data; 2 | use saku_lib::pkg::flask::Flask; 3 | 4 | use saku_lib::prelude::*; 5 | use saku_lib::util::msg; 6 | 7 | pub fn add_with_name(name: &str, url: &str) -> Result<()> { 8 | let flask = Flask::new(name, url)?; 9 | 10 | // install flask to flask/flasks/name 11 | data::save_pkg(flask.pkg())?; 12 | 13 | // install seeds to flask/name 14 | flask.link()?; 15 | 16 | Ok(()) 17 | } 18 | 19 | pub fn add(url: &str) -> Result<()> { 20 | let flask = Flask::from_url(url)?; 21 | msg::add_flask(&flask.name(), &flask.url()); 22 | 23 | // install flask to flask/flasks/owner.repo 24 | data::save_pkg(flask.pkg())?; 25 | 26 | // install seeds to flask/owner.repo 27 | flask.link()?; 28 | 29 | Ok(()) 30 | } 31 | 32 | pub fn update(url: &str) -> Result<()> { 33 | let flask = data::get_flask(url)?; 34 | 35 | crate::update::update_flask(&flask)?; 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /saku-cli/src/install.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::prelude::*; 2 | use saku_lib::exec; 3 | use saku_lib::pkg::config; 4 | use saku_lib::pkg::data::get_pkg; 5 | use saku_lib::pkg::pkg::Pkg; 6 | use saku_lib::util::io; 7 | use saku_lib::util::{msg, path}; 8 | 9 | pub fn install(pkg_name: &String) -> Result<()> { 10 | let p: Pkg = get_pkg(pkg_name)?; 11 | 12 | install_pkg(p)?; 13 | 14 | Ok(()) 15 | } 16 | 17 | pub fn clone_pkg(p: &Pkg) -> Result<()> { 18 | if path::exists(&path::repo(&p.name)) { 19 | info!("removing existing repo at {}", msg::general::path_f(&path::repo(&p.name))); 20 | io::rmdir(&path::repo(&p.name))?; 21 | } 22 | 23 | msg::clone(&p.name, &p.url); 24 | 25 | exec::Cmd::Clone{ url: p.url.clone(), name: p.name.clone() }.exec()?; 26 | 27 | Ok(()) 28 | } 29 | 30 | pub fn build_pkg(p: &Pkg) -> Result<()> { 31 | msg::build(&p.name, &path::repo(&p.name)); 32 | debug!("{:?}", &p); 33 | 34 | exec::build(&p.name, &p.group)?; 35 | 36 | Ok(()) 37 | } 38 | 39 | pub fn install_pkg(p: Pkg) -> Result<()> { 40 | clone_pkg(&p)?; 41 | 42 | build_pkg(&p)?; 43 | 44 | p.install_root()?; 45 | 46 | let config = config::Config::new()?; 47 | if !config.main.no_install_cleanup { 48 | exec::cleanup(&p.name, &p.group)?; 49 | } 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /saku-cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod env; 2 | pub mod config; 3 | pub mod show; 4 | pub mod changelog; 5 | pub mod search; 6 | pub mod install; 7 | pub mod add; 8 | pub mod remove; 9 | pub mod flask; 10 | pub mod uninstall; 11 | pub mod update; 12 | pub mod upgrade; 13 | pub mod list; 14 | 15 | extern crate saku_lib; 16 | -------------------------------------------------------------------------------- /saku-cli/src/list.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::pkg::data; 2 | use saku_lib::pkg::flask::Flask; 3 | use saku_lib::pkg::pkg::Pkg; 4 | use saku_lib::prelude::*; 5 | use saku_lib::util::{msg, path}; 6 | 7 | pub fn list() -> Result<()> { 8 | let flask_files = data::get_flasks()?; 9 | 10 | let flasks: Vec> = flask_files 11 | .iter() 12 | .map(|f| data::get_flask_from_name(f)) 13 | .collect(); 14 | 15 | for f in &flasks { 16 | if let Ok(flask) = f { 17 | println!(" - {} from {}", msg::general::name_f(&flask.name()), msg::general::url_f(&flask.url())); 18 | } else if let Err(err) = f { 19 | warn!("error while listing flask: {err}"); 20 | } 21 | } 22 | 23 | Ok(()) 24 | } 25 | 26 | pub fn list_installed() -> Result<()> { 27 | let dirs = path::get_store_dirs()?; 28 | 29 | let pkgs: Vec> = dirs 30 | .iter() 31 | .map(|name| data::get_pkg(&name)) 32 | .collect(); 33 | 34 | for p in &pkgs { 35 | if let Ok(pkg) = p { 36 | println!(" - {} from {}", msg::general::name_f(&pkg.name), msg::general::url_f(&pkg.url)); 37 | } else if let Err(err) = p { 38 | warn!("error while listing pkg: {err}"); 39 | } 40 | } 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /saku-cli/src/remove.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::util::msg; 2 | use saku_lib::util::path; 3 | use saku_lib::prelude::*; 4 | 5 | use std::fs; 6 | 7 | pub fn remove(name: &str) -> Result<()> { 8 | if path::pkg_exists("custom", name) { 9 | msg::remove(name); 10 | fs::remove_file(path::path_pkg("custom", name))?; 11 | return Ok(()); 12 | } 13 | 14 | Err(make_err!(NotFound, "package not found.")) 15 | } 16 | -------------------------------------------------------------------------------- /saku-cli/src/search.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::pkg::data; 2 | use saku_lib::prelude::*; 3 | use saku_lib::util::path; 4 | 5 | pub fn search(pattern: &str) -> Result<()> { 6 | let pkgs: Vec<[String;2]> = path::pkg_match(&pattern)?; 7 | if pkgs.len() == 0 { 8 | return Err(make_err!(NotFound, "no packages matching that name were found")) 9 | } 10 | 11 | pkgs.iter().map( 12 | |p| { 13 | let (group, name) = (p[0].clone(), p[1].clone()); 14 | debug!("found pkg {}/{}", &group, &name); 15 | let path = path::path_pkg(&group, &name); 16 | let pkg = data::get_pkg_from_path(&path)?; 17 | pkg.show()?; 18 | Ok(()) 19 | }).collect::>()?; 20 | 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /saku-cli/src/show.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::pkg::data::get_pkg; 2 | use saku_lib::prelude::*; 3 | 4 | pub fn show(pkg_name: &String) -> Result<()> { 5 | let pkg = get_pkg(pkg_name)?; 6 | pkg.show()?; 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /saku-cli/src/uninstall.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::prelude::*; 2 | 3 | use saku_lib::pkg::pkg::Pkg; 4 | use saku_lib::pkg::data; 5 | 6 | pub fn remove(name: &str) -> Result<()> { 7 | let p: Pkg = data::get_pkg(name)?; 8 | p.uninstall_root()?; 9 | p.cleanup_store()?; 10 | p.cleanup_repo()?; 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /saku-cli/src/update.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::pkg::data; 2 | use saku_lib::pkg::flask::Flask; 3 | use saku_lib::util::msg; 4 | use saku_lib::prelude::*; 5 | 6 | pub fn update_flask(flask: &Flask) -> Result<()> { 7 | msg::fetch(&flask.name(), &flask.url()); 8 | 9 | flask.update()?; 10 | 11 | Ok(()) 12 | } 13 | 14 | pub fn update_flask_from_url(url: &String) -> Result<()> { 15 | let flask = data::get_flask(&url)?; 16 | update_flask(&flask)?; 17 | Ok(()) 18 | } 19 | 20 | fn update_flask_from_name(name: &String) -> Result<()> { 21 | let flask = data::get_flask_from_name(&name)?; 22 | update_flask(&flask)?; 23 | Ok(()) 24 | } 25 | 26 | pub fn update() -> Result<()> { 27 | let flasks = data::get_flasks()?; 28 | flasks 29 | .iter() 30 | .map::, fn(&String) -> Result<()>>(update_flask_from_name) 31 | .collect::>()?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /saku-cli/src/upgrade.rs: -------------------------------------------------------------------------------- 1 | use saku_lib::exec; 2 | use saku_lib::prelude::*; 3 | 4 | use saku_lib::pkg::data; 5 | 6 | pub fn upgrade(name: &str) -> Result<()> { 7 | let pkg = data::get_pkg(name)?; 8 | 9 | exec::Cmd::Pull{ name: pkg.name.clone() }.exec()?; 10 | 11 | exec::upgrade(&pkg.name.clone(), &pkg.group.clone())?; 12 | 13 | pkg.install_root()?; 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /saku-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "saku-lib" 3 | version = "0.1.0" 4 | 5 | [lib] 6 | path = "lib.rs" 7 | 8 | [dependencies] 9 | log = "0.4" 10 | directories = "5.0.1" 11 | lazy_static = "1.4.0" 12 | regex = "1.9.6" 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_yaml = "0.9" 15 | toml = "0.8.2" 16 | nom = "7.1.3" 17 | glob = "0.3.1" 18 | fake-tty = "0.3.1" 19 | -------------------------------------------------------------------------------- /saku-lib/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::io; 3 | 4 | #[macro_export] 5 | macro_rules! make_err { 6 | () => { 7 | Error::default() 8 | }; 9 | ($t:ident, $v:literal) => { 10 | Error::$t(format!($v)) 11 | }; 12 | } 13 | 14 | #[derive(Debug, Default)] 15 | pub enum Error { 16 | NotFound(String), 17 | Missing(String), 18 | // io object already exists, multiple packages with similar names 19 | Conflict(String), 20 | IO(String), 21 | Regex(String), 22 | Parse(String), 23 | #[default] 24 | Unexpected, 25 | } 26 | 27 | impl fmt::Display for Error { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | let msg = match self { 30 | Error::NotFound(s) => format!("Not Found: {s}"), 31 | Error::Missing(s) => format!("Missing: {s}"), 32 | Error::Conflict(s) => format!("Conflict: {s}"), 33 | Error::IO(s) => format!("IO: {s}"), 34 | Error::Regex(s) => format!("Regex: {s}"), 35 | Error::Parse(s) => format!("Parse: {s}"), 36 | Error::Unexpected => format!("unexpected error"), 37 | }; 38 | write!(f, "{}", msg) 39 | } 40 | } 41 | 42 | impl From for Error { 43 | fn from(value: io::Error) -> Self { 44 | Self::IO(format!("io error {}", value.raw_os_error().unwrap())) 45 | } 46 | } 47 | 48 | impl From for Error { 49 | fn from(value: regex::Error) -> Self { 50 | Self::Regex(format!("regex error {}", value.to_string())) 51 | } 52 | } 53 | 54 | impl From for Error { 55 | fn from(value: glob::GlobError) -> Self { 56 | Self::IO(format!("glob error {}", value.to_string())) 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(value: glob::PatternError) -> Self { 62 | Self::IO(format!("glob error {}", value.to_string())) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /saku-lib/exec/cmd.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | use util::constants; 4 | use util::path; 5 | 6 | use super::run::run; 7 | 8 | pub enum Cmd { 9 | Clone { url: String, name: String }, 10 | Fetch { name: String }, 11 | Pull { name: String }, 12 | Log { name: String }, 13 | Link { target: String, path: String }, 14 | Unlink { path: String }, 15 | } 16 | 17 | impl Cmd { 18 | pub fn get_cmd(&self) -> Vec { 19 | match self { 20 | Cmd::Clone { url, name } => vec![format!( 21 | "git clone --filter=blob:none {} {}", 22 | url, 23 | path::repo(name) 24 | )], 25 | Cmd::Fetch { .. } => vec!["git fetch -q".to_string(), "git checkout -q".to_string()], 26 | Cmd::Pull { .. } => vec!["git pull".to_string()], 27 | Cmd::Log { .. } => vec!["git -c pager.show=false show --format=' - %C(yellow)%h%C(reset) %<(80,trunc)%s' -q @@{1}..@@{0}".to_string()], 28 | Cmd::Link { target, path } => vec![format!("ln -s {target} {path}")], 29 | Cmd::Unlink { path } => vec![format!("unlink {path}")], 30 | } 31 | } 32 | pub fn get_pwd(&self) -> String { 33 | match self { 34 | Cmd::Clone { .. } => constants::REPO_DIR.to_string(), 35 | Cmd::Fetch { name } => path::repo(name), 36 | Cmd::Pull { name } => path::repo(name), 37 | Cmd::Log { name } => path::repo(name), 38 | Cmd::Link { .. } => constants::SAKU_DIR.to_string(), 39 | Cmd::Unlink { .. } => constants::SAKU_DIR.to_string(), 40 | } 41 | } 42 | pub fn exec(self) -> Result<()> { 43 | run(self.get_cmd(), &self.get_pwd()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /saku-lib/exec/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::util; 2 | use crate::prelude::*; 3 | 4 | pub use self::cmd::Cmd; 5 | use self::run::run; 6 | 7 | pub mod cmd; 8 | mod run; 9 | pub mod pkg; 10 | 11 | pub fn install(name: &str, group: &str) -> Result<()> { 12 | pkg::run(pkg::Cmd::Install, name, group) 13 | } 14 | 15 | pub fn build(name: &str, group: &str) -> Result<()> { 16 | pkg::run(pkg::Cmd::Build, name, group) 17 | } 18 | 19 | pub fn upgrade(name: &str, group: &str) -> Result<()> { 20 | pkg::run(pkg::Cmd::Upgrade, name, group) 21 | } 22 | 23 | pub fn cleanup(name: &str, group: &str) -> Result<()> { 24 | pkg::run(pkg::Cmd::CleanUp, name, group) 25 | } 26 | 27 | pub fn run_in_repo(name: &str, cmd: Vec) -> Result<()> { 28 | run(cmd, &util::path::repo(name)) 29 | } 30 | 31 | pub fn run_in_root(prefix: &str, cmd: Vec) -> Result<()> { 32 | run(cmd, &util::path::root_dir(prefix)) 33 | } 34 | -------------------------------------------------------------------------------- /saku-lib/exec/pkg.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::util; 3 | use crate::util::constants; 4 | 5 | use super::run::run_one; 6 | 7 | pub fn init_cmd(name: &str, group: &str, function: &str) -> String { 8 | format!(". {} {group} {name} && {function}", *constants::INIT_FILE) 9 | } 10 | 11 | pub enum Cmd { 12 | Install, 13 | Build, 14 | Upgrade, 15 | CleanUp, 16 | } 17 | 18 | pub fn run(cmd: Cmd, name: &str, group: &str) -> Result<()> { 19 | let arg = match cmd { 20 | Cmd::Install => "_install", 21 | Cmd::Build => "_build", 22 | Cmd::Upgrade => "_upgrade", 23 | Cmd::CleanUp => "_cleanup", 24 | }; 25 | run_one(init_cmd(name, group, arg), &util::path::repo(name)) 26 | } 27 | -------------------------------------------------------------------------------- /saku-lib/exec/run.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::util::constants; 3 | 4 | use std::collections::HashMap; 5 | use std::io::{Read, stdout, Write, IoSlice}; 6 | use std::process::{Command, Stdio}; 7 | use std::thread; 8 | 9 | /// Pipe streams are blocking, we need separate threads to monitor them without blocking the primary thread. 10 | fn child_stream_to_vec(mut stream: R) -> Result<()> 11 | where 12 | R: Read + Send + 'static, 13 | { 14 | let out = stdout(); 15 | thread::Builder::new() 16 | .name("child_stream_to_vec".into()) 17 | .spawn(move || loop { 18 | let mut buf = [0]; 19 | match stream.read(&mut buf) { 20 | Err(err) => { 21 | println!("{}] Error reading from stream: {}", line!(), err); 22 | break; 23 | } 24 | Ok(got) => { 25 | if got == 0 { 26 | break; 27 | } else if got == 1 { 28 | let value = buf[0]; 29 | out.lock().write_vectored(&[IoSlice::new(&[value])]).unwrap(); 30 | } else { 31 | println!("{}] Unexpected number of bytes: {}", line!(), got); 32 | break; 33 | } 34 | } 35 | } 36 | })?; 37 | Ok(()) 38 | } 39 | 40 | pub fn run(cmd: Vec, pwd: &str) -> Result<()> { 41 | let mut cwd: String = pwd.to_string(); 42 | for c in cmd { 43 | let line = c.clone(); 44 | let elements = line.splitn(2, ' '); 45 | let el: Vec<&str> = elements.collect(); 46 | if el[0] == "cd" { 47 | cwd = el[1].to_string(); 48 | continue; 49 | } 50 | drop(el); 51 | let cmd: &mut Command = &mut fake_tty::bash_command("bash")?; 52 | cmd.arg("-c").arg(line); 53 | cmd.current_dir(cwd.as_str()); 54 | cmd.envs(env_vars()); 55 | 56 | let mut child = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?; 57 | child_stream_to_vec(child.stdout.take().expect("!stdout"))?; 58 | child_stream_to_vec(child.stderr.take().expect("!stderr"))?; 59 | child.wait()?; 60 | } 61 | Ok(()) 62 | } 63 | 64 | pub fn run_one(cmd: String, pwd: &str) -> Result<()> { 65 | run(vec![cmd], pwd) 66 | } 67 | 68 | fn env_vars() -> HashMap { 69 | let mut vars = HashMap::new(); 70 | vars.insert("SAKUPATH".to_string(), (&*constants::SAKU_DIR).into()); 71 | let log_env = ::std::env::var("RUST_LOG").unwrap_or("info".to_string()); 72 | vars.insert("RUST_LOG".to_string(), log_env); 73 | vars 74 | } 75 | -------------------------------------------------------------------------------- /saku-lib/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod prelude; 3 | 4 | pub mod util; 5 | pub mod exec; 6 | pub mod pkg; 7 | 8 | extern crate log; 9 | extern crate directories; 10 | extern crate lazy_static; 11 | extern crate regex; 12 | extern crate serde; 13 | extern crate serde_yaml; 14 | extern crate toml; 15 | extern crate nom; 16 | extern crate glob; 17 | extern crate fake_tty; 18 | -------------------------------------------------------------------------------- /saku-lib/pkg/config.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::prelude::*; 5 | use crate::util::constants; 6 | use crate::util::filepath; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct ConfigMain { 10 | // whether to update saku on `saku update`; true means no update 11 | pub frozen_update: bool, 12 | // whether to cleanup on `saku install`; true means no cleanup 13 | pub no_install_cleanup: bool, 14 | } 15 | 16 | impl ConfigMain { 17 | pub fn default() -> Self { 18 | Self { 19 | frozen_update: false, 20 | no_install_cleanup: false, 21 | } 22 | } 23 | } 24 | 25 | #[derive(Serialize, Deserialize)] 26 | pub struct Config { 27 | pub main: ConfigMain, 28 | } 29 | 30 | impl Config { 31 | pub fn path() -> Result { 32 | if let Some(path) = directories::ProjectDirs::from("dev", "comfysage", "saku") { 33 | let dir = path.config_dir(); 34 | let path = filepath::join(dir.to_str().ok_or(make_err!())?, &*constants::CONFIG_NAME); 35 | return Ok(path); 36 | } else { 37 | return Err(make_err!(IO, "no config dir.")); 38 | } 39 | } 40 | pub fn new() -> Result { 41 | let path = Self::path()?; 42 | if let Some(content) = fs::read_to_string(path).ok() { 43 | let config: Self = toml::from_str(&content).unwrap_or(Self::default()); 44 | Ok(config) 45 | } else { 46 | return Ok(Self::default()); 47 | } 48 | } 49 | pub fn to_string(&self) -> Result { 50 | toml::to_string(self).map_err(|_| make_err!(Parse, "couldn't create toml from string")) 51 | } 52 | } 53 | 54 | impl Default for Config { 55 | fn default() -> Self { 56 | Self { 57 | main: ConfigMain::default(), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /saku-lib/pkg/data.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use crate::util; 4 | use crate::prelude::*; 5 | use crate::util::path; 6 | use crate::util::url; 7 | 8 | use super::flask::Flask; 9 | use super::pkg::Pkg; 10 | use super::config::Config; 11 | 12 | /// Parse a flask file and return a *Pkg* 13 | /// 14 | /// - *path*: path to a flask file 15 | pub fn get_pkg_from_path(path: &str) -> Result { 16 | let fullpath = util::filepath::extend(path)?; 17 | 18 | let fh = fs::read_to_string(fullpath)?; 19 | let mut pkg = Pkg::from_string(fh)?; 20 | 21 | pkg.path = Some(path.to_string()); 22 | pkg.safe_guard()?; 23 | pkg.fill()?; 24 | 25 | Ok(pkg) 26 | } 27 | 28 | /// Parse a flask file and return a *Pkg* 29 | /// 30 | /// - *name*: name of a package 31 | pub fn get_pkg(name: &str) -> Result { 32 | let (path, is_dir): (String, bool) = util::path::path_determine(name.to_string())?; 33 | 34 | if is_dir { 35 | let pkg_path = util::path::repo_seed(&path); 36 | let mut pkg = get_pkg_from_path(&pkg_path)?; 37 | store_repo_seed(&mut pkg)?; 38 | 39 | return Ok(pkg); 40 | } 41 | 42 | let pkg = get_pkg_from_path(&path)?; 43 | Ok(pkg) 44 | } 45 | 46 | /// Write a *Pkg* to a flask file 47 | /// 48 | /// - *pkg*: package to save 49 | pub fn save_pkg(pkg: &Pkg) -> Result<()> { 50 | let str = pkg.to_string()?; 51 | 52 | let group: &str = if pkg.group.len() == 0 { 53 | "custom" 54 | } else { 55 | &pkg.group 56 | }; 57 | 58 | let path = util::path::path_pkg(group, &pkg.name); 59 | debug!("saving pkg to {:?}", &path); 60 | fs::write(path, str)?; 61 | 62 | Ok(()) 63 | } 64 | 65 | /// Write a *Pkg* to flask file for a repo package 66 | /// 67 | /// - *pkg*: package to save 68 | fn store_repo_seed(pkg: &mut Pkg) -> Result<()> { 69 | pkg.group = format!("repo"); 70 | 71 | let _ = util::io::mkdir(util::path::gr("repo"))?; 72 | 73 | save_pkg(pkg)?; 74 | 75 | Ok(()) 76 | } 77 | 78 | /// Write a *Config* to the config file 79 | /// 80 | /// - *config*: config to save 81 | /// 82 | /// config file is located at `~/.config/saku/saku.toml` 83 | pub fn save_config(config: Config) -> Result<()> { 84 | let str = config.to_string()?; 85 | 86 | let path = Config::path()?; 87 | fs::write(path, str)?; 88 | 89 | Ok(()) 90 | } 91 | 92 | /// Parse a flask file and return a *Flask* 93 | /// 94 | /// - *url*: url associated with flask 95 | pub fn get_flask(url: &str) -> Result { 96 | let name = url::url_name(url)?; 97 | let flask = get_flask_from_name(&name)?; 98 | Ok(flask) 99 | } 100 | 101 | /// Parse a flask file and return a *Flask* 102 | /// 103 | /// - *name*: name of a flask 104 | pub fn get_flask_from_name(name: &str) -> Result { 105 | let path = path::flask(name); 106 | let pkg = get_pkg_from_path(&path)?; 107 | let flask = Flask::from_pkg(pkg)?; 108 | Ok(flask) 109 | } 110 | 111 | /// Search for all flasks 112 | /// returns a list of names 113 | /// 114 | /// searches in `~/.saku/flask/flasks` 115 | pub fn get_flasks() -> Result> { 116 | let files = path::flasks()?; 117 | let flasks = files.iter().map(|s| path::remove_extension(s.to_string())).collect(); 118 | 119 | Ok(flasks) 120 | } 121 | -------------------------------------------------------------------------------- /saku-lib/pkg/flask.rs: -------------------------------------------------------------------------------- 1 | use crate::exec; 2 | use crate::util::{constants, io, path, url}; 3 | use crate::prelude::*; 4 | 5 | use crate::pkg::pkg::Pkg; 6 | 7 | // examples: 8 | // Flask({ name: "comfysage.core", url: "comfysage/core" }, "comfysage/core") 9 | // Flask({ name: "aur.archlinux.org.pkg", url: "aur.archlinux.org/pkg" }, "aur.archlinux.org/pkg") 10 | 11 | #[derive(Debug)] 12 | pub struct Flask(Pkg, String); 13 | 14 | impl Flask { 15 | pub fn new(name: &str, url: &str) -> Result { 16 | let shortened = url::shorten_url(url)?; 17 | let pkg = Flask::new_pkg(name, url)?; 18 | Ok(Flask(pkg, shortened)) 19 | } 20 | pub fn from_url(url: &str) -> Result { 21 | let shortened = url::shorten_url(url)?; 22 | let name = url::url_name(url)?; 23 | let pkg = Flask::new_pkg(&name, url)?; 24 | Ok(Flask(pkg, shortened)) 25 | } 26 | pub fn from_pkg(pkg: Pkg) -> Result { 27 | let shortened = url::shorten_url(&pkg.url)?; 28 | Ok(Flask(pkg, shortened)) 29 | } 30 | } 31 | 32 | impl Flask { 33 | fn new_pkg(name: &str, url: &str) -> Result { 34 | let mut pkg = Pkg::new(name, url); 35 | pkg.desc = format!("flasks from {url}"); 36 | pkg.group = constants::FLASK_DIR_NAME.to_string(); 37 | pkg.fill()?; 38 | Ok(pkg) 39 | } 40 | } 41 | 42 | impl Flask { 43 | pub fn name(&self) -> String { 44 | self.pkg().name.clone() 45 | } 46 | pub fn url(&self) -> String { 47 | self.1.clone() 48 | } 49 | pub fn pkg(&self) -> &Pkg { 50 | &self.0 51 | } 52 | 53 | pub fn full_url(&self) -> Result { 54 | let url = &self.url(); 55 | let full = url::extend_url(url)?; 56 | Ok(full) 57 | } 58 | } 59 | 60 | impl Flask { 61 | pub fn link(&self) -> Result<()> { 62 | // NOTE: creates link `flask/owner.repo -> repo/owner.repo/flasks` 63 | let name = &self.name(); 64 | io::link(&path::flask_dir(name), &path::gr(name))?; 65 | Ok(()) 66 | } 67 | 68 | // pull from git repo 69 | pub fn update(&self) -> Result<()> { 70 | let name = &self.name(); 71 | if path::repo_exists(name) { 72 | exec::Cmd::Pull{ name: name.to_string() }.exec() 73 | } else { 74 | exec::Cmd::Clone{ url: self.full_url()?, name: name.to_string() }.exec() 75 | } 76 | } 77 | } 78 | 79 | mod test { 80 | #![allow(unused_imports)] 81 | use crate::prelude::*; 82 | 83 | use super::Flask; 84 | 85 | #[test] 86 | fn new() -> Result<()> { 87 | let (name, url) = ("comfysage.core", "comfysage/core"); 88 | let flask = Flask::new(name, url)?; 89 | assert_eq!(flask.pkg().name, name); 90 | assert_eq!(flask.url(), url); 91 | 92 | let (name, url) = ("core", "comfysage/core"); 93 | let flask = Flask::new(name, url)?; 94 | assert_eq!(flask.pkg().name, name); 95 | assert_eq!(flask.url(), url); 96 | 97 | Ok(()) 98 | } 99 | #[test] 100 | fn from_url() -> Result<()> { 101 | let (name, url) = ("comfysage.core", "comfysage/core"); 102 | let flask = Flask::from_url(url)?; 103 | assert_eq!(flask.pkg().name, name); 104 | assert_eq!(flask.url(), url); 105 | 106 | let (name, url) = ("comfysage.core", "comfysage/core"); 107 | let flask = Flask::from_url(url)?; 108 | assert_eq!(flask.pkg().name, name); 109 | assert_eq!(flask.url(), url); 110 | 111 | Ok(()) 112 | } 113 | #[test] 114 | fn from_pkg() -> Result<()> { 115 | let (name, url) = ("comfysage.core", "comfysage/core"); 116 | let flask = Flask::new(name, url)?; 117 | assert_eq!(flask.pkg().name, name); 118 | assert_eq!(flask.url(), url); 119 | 120 | let pkg = flask.pkg().clone(); 121 | let flask_pkg = Flask::from_pkg(pkg)?; 122 | assert_eq!(flask_pkg.pkg().name, name); 123 | assert_eq!(flask_pkg.url(), url); 124 | 125 | let (name, url) = ("core", "comfysage/core"); 126 | let flask = Flask::new(name, url)?; 127 | assert_eq!(flask.pkg().name, name); 128 | assert_eq!(flask.url(), url); 129 | 130 | let pkg = flask.pkg().clone(); 131 | let flask_pkg = Flask::from_pkg(pkg)?; 132 | assert_eq!(flask_pkg.pkg().name, name); 133 | assert_eq!(flask_pkg.url(), url); 134 | 135 | Ok(()) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /saku-lib/pkg/flaskfile.rs: -------------------------------------------------------------------------------- 1 | // A library to parse PKGBUILD files with Rust and Nom 2 | 3 | use nom::{ 4 | branch::permutation, 5 | bytes::complete::{tag, take_until}, 6 | character::complete::space0, 7 | IResult, 8 | }; 9 | 10 | #[derive(Debug, PartialEq)] 11 | pub struct PkgBuild { 12 | //mandatory fields 13 | pub pkgname: String, 14 | pub pkgdesc: String, 15 | pub url: String, 16 | } 17 | 18 | impl PkgBuild { 19 | // parsing mandatory fields in any order 20 | pub fn parse(input: &str) -> IResult<&str, PkgBuild> { 21 | permutation(( 22 | PkgBuild::parse_pkgname, 23 | PkgBuild::parse_pkgdesc, 24 | PkgBuild::parse_url, 25 | ))(input) 26 | .map(|(next_input, (pkgname, pkgdesc, url))| { 27 | ( 28 | next_input, 29 | PkgBuild { 30 | pkgname, 31 | pkgdesc, 32 | url, 33 | }, 34 | ) 35 | }) 36 | } 37 | 38 | fn parse_field<'a>(input: &'a str, field: &str) -> IResult<&'a str, String> { 39 | let (input, _) = tag(field)(input)?; 40 | let (input, _) = space0(input)?; 41 | let (input, _) = tag("=")(input)?; 42 | let (input, _) = space0(input)?; 43 | let (input, value) = take_until("\n")(input)?; 44 | let (input, _) = tag("\n")(input)?; 45 | Ok((input, value.to_string().trim_matches('"').to_string())) 46 | } 47 | 48 | fn parse_pkgname(input: &str) -> IResult<&str, String> { 49 | Self::parse_field(input, "pkgname") 50 | } 51 | 52 | fn parse_pkgdesc(input: &str) -> IResult<&str, String> { 53 | Self::parse_field(input, "pkgdesc") 54 | } 55 | 56 | fn parse_url(input: &str) -> IResult<&str, String> { 57 | Self::parse_field(input, "url") 58 | } 59 | 60 | pub fn to_string(&self) -> String { 61 | format!("pkgname=\"{}\"\nurl=\"{}\"\npkgdesc=\"{}\"\n", self.pkgname, self.url, self.pkgdesc) 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | #[test] 68 | fn pkgname() { 69 | let input = "pkgname=foo\n"; 70 | let expected = "foo"; 71 | let (input, pkgname) = super::PkgBuild::parse_pkgname(input).unwrap(); 72 | assert_eq!(input, ""); 73 | assert_eq!(pkgname, expected); 74 | } 75 | 76 | #[test] 77 | fn pkgdesc() { 78 | let input = "pkgdesc=bar\n"; 79 | let expected = "bar"; 80 | let (input, pkgdesc) = super::PkgBuild::parse_pkgdesc(input).unwrap(); 81 | assert_eq!(input, ""); 82 | assert_eq!(pkgdesc, expected); 83 | } 84 | 85 | #[test] 86 | fn url() { 87 | let input = "url=foo/bar\n"; 88 | let expected = "foo/bar"; 89 | let (input, url) = super::PkgBuild::parse_url(input).unwrap(); 90 | assert_eq!(input, ""); 91 | assert_eq!(url, expected); 92 | } 93 | 94 | #[test] 95 | fn pkgbuild() { 96 | let input = "pkgname=foo\npkgdesc=bar\nurl=foo/bar\n"; 97 | let expected = super::PkgBuild { 98 | pkgname: "foo".to_string(), 99 | pkgdesc: "bar".to_string(), 100 | url: "foo/bar".to_string(), 101 | }; 102 | let (input, pkgbuild) = super::PkgBuild::parse(input).unwrap(); 103 | assert_eq!(input, ""); 104 | assert_eq!(pkgbuild, expected); 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /saku-lib/pkg/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod root; 3 | pub mod rebuild; 4 | pub mod flaskfile; 5 | pub mod pkg; 6 | pub mod data; 7 | pub mod flask; 8 | -------------------------------------------------------------------------------- /saku-lib/pkg/pkg.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::util::url::extend_url; 3 | use crate::util::{self, filepath, path}; 4 | use crate::util::{msg, url}; 5 | 6 | use crate::pkg::flaskfile::PkgBuild; 7 | 8 | use serde::{Serialize, Deserialize}; 9 | 10 | /// a package struct 11 | /// 12 | /// - *name*: name of the package 13 | /// - *desc*: short description 14 | /// - *url*: url of the repo 15 | /// - *group*: flask group of the package 16 | /// - *path*: path to the flask file of the package 17 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 18 | pub struct Pkg { 19 | #[serde(rename = "pkg")] 20 | pub name: String, 21 | pub desc: String, 22 | pub url: String, 23 | 24 | #[serde(skip_serializing, default)] 25 | pub group: String, 26 | #[serde(default)] 27 | pub path: Option, 28 | } 29 | 30 | impl Pkg { 31 | /// Create a new *Pkg* 32 | /// 33 | /// - *name*: name of the package 34 | /// - *url*: url of the repo 35 | pub fn new(name: &str, url: &str) -> Self { 36 | Self { 37 | name: format!("{name}"), 38 | url: format!("{url}"), 39 | desc: format!(""), 40 | group: format!(""), 41 | path: None, 42 | } 43 | } 44 | } 45 | 46 | // meta 47 | impl Pkg { 48 | /// Auto-fill *Pkg* fields 49 | /// 50 | /// - *group* is infered from the path to the flask file 51 | /// - *url* is extended using `util::url::extend_url` 52 | pub fn fill(&mut self) -> Result<()> { 53 | let local_path = self.get_path()?; 54 | self.infer_group(local_path)?; 55 | // infer url 56 | let url = extend_url(&self.url)?; 57 | self.url = url; 58 | 59 | Ok(()) 60 | } 61 | /// Infer group of a package 62 | /// 63 | /// group is set to the dirname of the flask file of the package 64 | pub fn infer_group(&mut self, path: String) -> Result<()> { 65 | if self.group.len() > 0 { 66 | return Ok(()); 67 | } 68 | let cwd = util::cli::get_cwd()?; 69 | let abspath = filepath::abs(&filepath::join(&cwd, &path)); 70 | 71 | let abs_bind = abspath?; 72 | let sp = abs_bind.split("/").collect::>(); 73 | 74 | let required_index = 2; 75 | if sp.len() < required_index { 76 | return Err(make_err!(NotFound, "path to pkg not long enough")); 77 | } 78 | self.group = sp[sp.len() - required_index].to_string(); 79 | Ok(()) 80 | } 81 | pub fn get_path(&mut self) -> Result { 82 | match &self.path { 83 | Some(s) => Ok(s.to_string()), 84 | None => { 85 | if self.group.len() > 0 { 86 | Ok(path::path_pkg(&self.group, &self.name)) 87 | } else{ 88 | path::pkg_search(&self.name) 89 | } 90 | }, 91 | } 92 | } 93 | } 94 | 95 | // data 96 | impl Pkg { 97 | pub fn from_string(str: String) -> Result { 98 | let (_, build) = PkgBuild::parse(&str).map_err(|e| { 99 | let e = e.to_string(); 100 | make_err!(Parse, "{e}") 101 | })?; 102 | let pkg = Self { 103 | name: build.pkgname, 104 | url: build.url, 105 | desc: build.pkgdesc, 106 | group: "".to_string(), 107 | path: None, 108 | }; 109 | Ok(pkg) 110 | } 111 | pub fn to_string(&self) -> Result { 112 | let build = PkgBuild { 113 | pkgname: self.name.to_string(), 114 | pkgdesc: self.desc.to_string(), 115 | url: self.url.to_string(), 116 | }; 117 | Ok(PkgBuild::to_string(&build)) 118 | } 119 | } 120 | 121 | // safeguard 122 | impl Pkg { 123 | // error checking for pkg to avoid unwanted behaviour 124 | pub fn safe_guard(&self) -> Result<()> { 125 | // require pkg name 126 | if self.name.len() < 1 { 127 | return Err(make_err!(Missing, "no pkg name specified.")); 128 | } 129 | 130 | // require url 131 | if self.url.len() < 1 { 132 | return Err(make_err!(Missing, "no url specified.")); 133 | } 134 | 135 | Ok(()) 136 | } 137 | } 138 | 139 | impl Pkg { 140 | pub fn show(&self) -> Result<()> { 141 | println!("{}", msg::general::present_name(&self.name, &self.group)); 142 | 143 | if self.desc.len() > 0 { 144 | println!("{}", self.desc); 145 | } 146 | if self.url.len() > 0 { 147 | println!( 148 | "url {}", 149 | msg::general::url_f(&url::shorten_url(&self.url)?) 150 | ); 151 | } 152 | Ok(()) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /saku-lib/pkg/rebuild.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | use crate::util::constants; 4 | use crate::util::io; 5 | use crate::util::msg; 6 | use crate::util::{filepath, path}; 7 | 8 | use super::pkg::Pkg; 9 | 10 | #[derive(Debug)] 11 | struct Node { 12 | /// store dir 13 | store_dir: String, 14 | /// as '/share/path/to/file' 15 | pub path: String, 16 | children: Vec, 17 | } 18 | 19 | impl Node { 20 | pub fn create_root(path: &str) -> Self { 21 | Node { 22 | store_dir: path.to_string(), 23 | path: String::from(""), 24 | children: vec![], 25 | } 26 | } 27 | pub fn new(store_dir: &str, path: &str) -> Self { 28 | Self { 29 | store_dir: store_dir.to_string(), 30 | children: vec![], 31 | path: path.to_string(), 32 | } 33 | } 34 | fn from(store_dir: &str, path: &str) -> Result { 35 | let rel = filepath::get_relative(store_dir, path)?; 36 | Ok(Self::new(store_dir, &rel)) 37 | } 38 | fn create(&self, path: &str) -> Result { 39 | let mut child = Node::from(&self.store_dir, &path)?; 40 | child.build()?; 41 | Ok(child) 42 | } 43 | fn push(&mut self, path: &str) -> Result<()> { 44 | let child = self.create(path)?; 45 | self.children.push(child); 46 | Ok(()) 47 | } 48 | fn abs(&self) -> String { 49 | filepath::join(&self.store_dir, &self.path) 50 | } 51 | pub fn has(&self, rel: &str) -> Result { 52 | if self.path == rel { 53 | debug!("{} has {:?}", self.store_dir, rel); 54 | return Ok(true); 55 | } 56 | if self.path.is_empty() || rel.find(&self.path) == Some(0) { 57 | debug!("{} contains {:?}", self.path, rel); 58 | for child in &self.children { 59 | if child.has(rel)? { 60 | return Ok(true); 61 | } 62 | } 63 | } 64 | Ok(false) 65 | } 66 | pub fn build(&mut self) -> Result<()> { 67 | if self.is_bud() { 68 | return Ok(()); 69 | } 70 | for entry in std::fs::read_dir(&self.abs())? 71 | .into_iter() 72 | .flatten() 73 | .map(|x| x.path()) 74 | .map(|x| x.to_str().map(|x| x.to_string())) 75 | .flatten() 76 | { 77 | self.push(&entry)?; 78 | } 79 | Ok(()) 80 | } 81 | fn is_bud(&self) -> bool { 82 | let is_dir = filepath::is_dir(&self.abs()); 83 | !is_dir 84 | } 85 | /// link node to root 86 | pub fn link(&self, root: &Node) -> Result<()> { 87 | trace!("({:?}) current node", &self.path); 88 | let path = filepath::join(&root.store_dir, &self.path); 89 | if self.is_bud() { 90 | if filepath::exists(&path) { 91 | msg::remove_file(&path); 92 | std::fs::remove_file(&path)?; 93 | } 94 | self.link_bud(root)?; 95 | } else { 96 | if root.has(&self.path)? { 97 | for child in &self.children { 98 | child.link(root)?; 99 | } 100 | } else { 101 | if filepath::exists(&path) { 102 | msg::remove_file(&path); 103 | std::fs::remove_file(&path)?; 104 | } 105 | self.link_bud(root)?; 106 | } 107 | } 108 | Ok(()) 109 | } 110 | fn link_bud(&self, root: &Node) -> Result<()> { 111 | let path = filepath::join(&root.store_dir, &self.path); 112 | if filepath::exists(&path) { 113 | return Err(make_err!(IO, "file already exists {path}")); 114 | } 115 | io::mkdir(filepath::parent_dir(&path)?)?; 116 | msg::link(&self.abs(), &path); 117 | io::link(&self.abs(), &path)?; 118 | Ok(()) 119 | } 120 | 121 | fn has_linked(&self, root: &Node) -> Result { 122 | let rel = &self.path; 123 | let root_path = filepath::join(&root.store_dir, &rel); 124 | if filepath::exists(&root_path) { 125 | let metadata = std::fs::symlink_metadata(&root_path)?; 126 | let filetype = metadata.file_type(); 127 | let is_symlink = filetype.is_symlink(); 128 | return Ok(is_symlink); 129 | } 130 | Ok(false) 131 | } 132 | pub fn unlink(&self, root: &Node) -> Result<()> { 133 | trace!("({}) current node", &self.path); 134 | if self.has_linked(root)? { 135 | let rel = &self.path; 136 | let root_path = filepath::join(&root.store_dir, &rel); 137 | msg::remove_file(&root_path); 138 | std::fs::remove_file(&root_path)?; 139 | return Ok(()); 140 | } 141 | if !self.is_bud() { 142 | for child in self.children.iter() { 143 | child.unlink(root)?; 144 | } 145 | } 146 | Ok(()) 147 | } 148 | } 149 | 150 | #[derive(Debug)] 151 | pub struct Root(Node, Node); 152 | 153 | impl Root { 154 | pub fn new(pkg: &Pkg) -> Result { 155 | let store_path = path::store_dir(&pkg.name); 156 | let root_path = &*constants::ROOT_DIR; 157 | if !filepath::exists(&store_path) { 158 | return Err(make_err!(IO, "store dir does not exist {store_path}")); 159 | } 160 | Ok(Self( 161 | Node::create_root(&store_path), 162 | Node::create_root(&root_path), 163 | )) 164 | } 165 | pub fn build(&mut self) -> Result<&Self> { 166 | self.0.build()?; 167 | self.1.build()?; 168 | Ok(self) 169 | } 170 | pub fn link(&self) -> Result<()> { 171 | self.0.link(&self.1)?; 172 | Ok(()) 173 | } 174 | pub fn uninstall(&self) -> Result<()> { 175 | self.0.unlink(&self.1)?; 176 | Ok(()) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /saku-lib/pkg/root.rs: -------------------------------------------------------------------------------- 1 | use crate::pkg::rebuild::Root; 2 | use crate::prelude::*; 3 | use crate::exec; 4 | use crate::pkg::pkg::Pkg; 5 | use crate::util::io; 6 | use crate::util::msg; 7 | use crate::util::path; 8 | 9 | impl Pkg { 10 | /// run install task 11 | /// - package files in store dir 12 | /// - link stored files to root 13 | pub fn install_root(&self) -> Result<()> { 14 | self.store()?; 15 | self.link_root()?; 16 | Ok(()) 17 | } 18 | /// package files in store dir 19 | pub fn store(&self) -> Result<()> { 20 | trace!("storing files"); 21 | let has_artifacts = !io::mkdir(path::store_dir(&self.name))?; 22 | if has_artifacts { 23 | debug!("cleaning up artifacts in store"); 24 | let files = path::get_stored_bin(&self.name)?; 25 | for entry in files { 26 | debug!("removing artifact {}", msg::general::path_f(&entry)); 27 | exec::Cmd::Unlink{ path: entry }.exec()?; 28 | } 29 | let store_path = path::store_dir(&self.name); 30 | let dirs = path::get_artifact_dirs(&self.name)?; 31 | for entry in dirs { 32 | debug!("removing artifact {}", msg::general::path_f(&entry)); 33 | io::rmdir(&entry)?; 34 | } 35 | io::mkdir(store_path)?; 36 | } 37 | exec::install(&self.name, &self.group)?; 38 | Ok(()) 39 | } 40 | /// link stored files to root 41 | pub fn link_root(&self) -> Result<()> { 42 | trace!("linking root"); 43 | Root::new(&self)?.build()?.link()?; 44 | Ok(()) 45 | } 46 | /// remove stored files in root 47 | pub fn uninstall_root(&self) -> Result<()> { 48 | trace!("uninstalling pkg from root"); 49 | Root::new(&self)?.build()?.uninstall()?; 50 | Ok(()) 51 | } 52 | /// remove store path for pkg 53 | pub fn cleanup_store(&self) -> Result<()> { 54 | trace!("cleaning store"); 55 | let store_path = path::store_dir(&self.name); 56 | msg::remove_file(&store_path); 57 | io::rmdir(&store_path)?; 58 | Ok(()) 59 | } 60 | /// remove repo path for pkg 61 | pub fn cleanup_repo(&self) -> Result<()> { 62 | trace!("cleaning repo"); 63 | let repo_path = path::repo(&self.name); 64 | msg::remove_file(&repo_path); 65 | io::rmdir(&repo_path)?; 66 | Ok(()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /saku-lib/prelude.rs: -------------------------------------------------------------------------------- 1 | // Re-export the crate Error. 2 | pub use crate::error::Error; 3 | 4 | pub use crate::make_err; 5 | 6 | // Alias Result to be the crate Result. 7 | pub type Result = core::result::Result; 8 | 9 | pub use log::{debug, error, info, trace, warn}; 10 | 11 | // Generic Wrapper tuple struct for newtype pattern, 12 | // mostly for external type to type From/TryFrom conversions 13 | pub struct W(pub T); 14 | -------------------------------------------------------------------------------- /saku-lib/util/cli.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use crate::prelude::*; 4 | 5 | pub fn get_cwd() -> Result { 6 | let working_dir = env::current_dir(); 7 | Ok(working_dir.map_err(|_| make_err!())?.to_str().unwrap().to_owned()) 8 | } 9 | -------------------------------------------------------------------------------- /saku-lib/util/colors.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | pub static COLOR_RED: &str = "\x1b[31m"; 4 | pub static COLOR_GREEN: &str = "\x1b[32m"; 5 | pub static COLOR_YELLOW: &str = "\x1b[33m"; 6 | pub static COLOR_BLUE: &str = "\x1b[34m"; 7 | pub static COLOR_MAGENTA: &str = "\x1b[35m"; 8 | pub static COLOR_CYAN: &str = "\x1b[36m"; 9 | pub static COLOR_RESET: &str = "\x1b[0m"; 10 | -------------------------------------------------------------------------------- /saku-lib/util/constants.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | 3 | lazy_static! { 4 | pub static ref HOME: String = std::env::var("HOME").unwrap(); 5 | pub static ref SAKU_PATH: String = std::env::var("SAKUPATH").unwrap_or(format!("{}{}", *HOME, "/.saku")); 6 | 7 | pub static ref SAKU_DIR: String = SAKU_PATH.to_string(); 8 | 9 | pub static ref CONFIG_NAME: String = "saku.toml".to_string(); 10 | pub static ref STORE_NAME: String = ".store.yaml".to_string(); 11 | 12 | pub static ref REPO_DIR: String = format!("{}{}", *SAKU_DIR, "/repo"); 13 | pub static ref PKG_DIR: String = format!("{}{}", *SAKU_DIR, "/flask"); 14 | pub static ref ROOT_DIR: String = format!("{}{}", *SAKU_DIR, "/root"); 15 | pub static ref STORE_DIR: String = format!("{}{}", *SAKU_DIR, "/store"); 16 | pub static ref LIB_DIR_NAME: String = "lib".to_string(); 17 | pub static ref LIB_DIR: String = format!("{}/{}", *SAKU_DIR, *LIB_DIR_NAME); 18 | 19 | pub static ref FLASK_DIR_NAME: String = "flasks".to_string(); 20 | pub static ref FLASK_DIR: String = format!("{}/{}", *PKG_DIR, *FLASK_DIR_NAME); 21 | 22 | pub static ref INIT_FILE_NAME: String = "init.fl".to_string(); 23 | pub static ref INIT_FILE: String = format!("{}/{}", *LIB_DIR, *INIT_FILE_NAME); 24 | 25 | pub static ref REPO_SEED: String = "pkg.fl".to_string(); 26 | } 27 | -------------------------------------------------------------------------------- /saku-lib/util/filepath.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use super::cli::{get_cwd, self}; 5 | 6 | pub fn join(path: &str, suffix: &str) -> String { 7 | Path::new(path).join(suffix).to_str().unwrap().to_string() 8 | } 9 | 10 | pub fn abs(path: &str) -> Result { 11 | let path = Path::new(path); 12 | let mut components = path.components(); 13 | 14 | let mut normalized = if path.is_absolute() { 15 | // "If a pathname begins with two successive characters, the 16 | // first component following the leading characters may be 17 | // interpreted in an implementation-defined manner, although more than 18 | // two leading characters shall be treated as a single 19 | // character." 20 | if path.starts_with("//") && !path.starts_with("///") { 21 | components.next(); 22 | PathBuf::from("//") 23 | } else { 24 | PathBuf::new() 25 | } 26 | } else { 27 | PathBuf::from(cli::get_cwd()?) 28 | }; 29 | normalized.extend(components); 30 | 31 | // "Interfaces using pathname resolution may specify additional constraints 32 | // when a pathname that does not name an existing directory contains at 33 | // least one non- character and contains one or more trailing 34 | // characters". 35 | // A trailing is also meaningful if "a symbolic link is 36 | // encountered during pathname resolution". 37 | if path.ends_with("/") { 38 | normalized.push(""); 39 | } 40 | 41 | Ok(normalized.to_str().unwrap().to_string()) 42 | } 43 | 44 | pub fn exists(path: &str) -> bool { 45 | let p = Path::new(&path); 46 | p.exists() 47 | } 48 | 49 | pub fn is_dir(path: &str) -> bool { 50 | let p = Path::new(&path); 51 | p.is_dir() 52 | } 53 | 54 | pub fn extend(path: &str) -> Result { 55 | if path.len() == 0 { 56 | return Err(make_err!(Missing, "path not long enough")); 57 | } 58 | 59 | // absolute path 60 | if path.starts_with("/") { 61 | return Ok(path.to_string()); 62 | } 63 | 64 | let cwd = get_cwd()?; 65 | let extended = join(&cwd, path); 66 | let abs_path = abs(&extended)?; 67 | 68 | Ok(abs_path) 69 | } 70 | 71 | pub fn base_name(path: &str) -> Result { 72 | Ok(Path::new(path) 73 | .file_name() 74 | .ok_or(Error::Unexpected)? 75 | .to_str() 76 | .ok_or(Error::Unexpected)? 77 | .to_string()) 78 | } 79 | 80 | pub fn parent_dir(path: &str) -> Result { 81 | Ok(Path::new(path) 82 | .parent() 83 | .ok_or(Error::Unexpected)? 84 | .to_str() 85 | .ok_or(Error::Unexpected)? 86 | .to_string()) 87 | } 88 | 89 | /// get path relative to root 90 | /// returns `Error::Unexpected` if root is not in path 91 | pub fn get_relative(root: &str, path: &str) -> Result { 92 | let path: String = abs(path)?; 93 | let rel = path.trim_start_matches(&format!("{root}/")).to_string(); 94 | Ok(rel) 95 | } 96 | -------------------------------------------------------------------------------- /saku-lib/util/io.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | use super::path; 4 | 5 | pub fn mkdir(path: String) -> Result { 6 | if path::exists(&path) { 7 | return Ok(false); 8 | } 9 | 10 | std::fs::create_dir_all(path)?; 11 | 12 | Ok(true) 13 | } 14 | 15 | pub fn rmdir(path: &str) -> Result<()> { 16 | if path::exists(&path) { 17 | // [NOTE]: removes a directory after cleaning its contents or removes a symlink 18 | std::fs::remove_dir_all(&path)?; 19 | } 20 | Ok(()) 21 | } 22 | 23 | pub fn link(target: &str, path: &str) -> Result<()> { 24 | std::os::unix::fs::symlink(target, path)?; 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /saku-lib/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod filepath; 3 | pub mod path; 4 | mod colors; 5 | pub mod msg; 6 | pub mod cli; 7 | pub mod io; 8 | pub mod url; 9 | -------------------------------------------------------------------------------- /saku-lib/util/msg.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use super::colors::{COLOR_BLUE, COLOR_CYAN, COLOR_MAGENTA, COLOR_RESET, COLOR_YELLOW}; 3 | 4 | pub mod general { 5 | use super::{COLOR_BLUE, COLOR_CYAN, COLOR_MAGENTA, COLOR_RESET, COLOR_YELLOW}; 6 | pub fn name_f(name: &str) -> String { 7 | format!("{COLOR_MAGENTA}{}{COLOR_RESET}", name) 8 | } 9 | 10 | pub fn present_name(name: &str, group: &str) -> String { 11 | format!("{COLOR_CYAN}{}/{}", group, name_f(name)) 12 | } 13 | 14 | pub fn url_f(url: &str) -> String { 15 | format!("{COLOR_YELLOW}{url}{COLOR_RESET}") 16 | } 17 | 18 | pub fn path_f(path: &str) -> String { 19 | format!("{COLOR_BLUE}{path}{COLOR_RESET}") 20 | } 21 | } 22 | 23 | pub fn clone(name: &str, url: &str) { 24 | info!( 25 | "{}", 26 | format!( 27 | "cloning {} from {} ...", 28 | general::name_f(name), 29 | general::url_f(url) 30 | ) 31 | ); 32 | } 33 | 34 | pub fn build(name: &str, path: &str) { 35 | info!( 36 | "{}", 37 | format!( 38 | "building {} at {} ...", 39 | general::name_f(name), 40 | general::path_f(path) 41 | ) 42 | ); 43 | } 44 | 45 | pub fn fetch(name: &str, url: &str) { 46 | info!( 47 | "{}", 48 | format!( 49 | "fetching {} from {} ...", 50 | general::name_f(name), 51 | general::url_f(url) 52 | ) 53 | ) 54 | } 55 | 56 | pub fn create_config(path: &str) { 57 | info!( 58 | "{}", 59 | format!( 60 | "creating config file at {} ...", 61 | general::path_f(path) 62 | ) 63 | ) 64 | } 65 | 66 | pub fn add(name: &str) { 67 | info!("{}", format!("adding {}", general::name_f(name))) 68 | } 69 | 70 | pub fn remove(name: &str) { 71 | info!("{}", format!("removing {}", general::name_f(name))) 72 | } 73 | 74 | pub fn add_flask(name: &str, url: &str) { 75 | info!("{}", format!("adding flask {} from {}", general::name_f(name), general::url_f(url))) 76 | } 77 | 78 | pub fn changelog(name: &str, path: &str) { 79 | info!("{}", format!("showing changes for {} at {}", general::name_f(name), general::path_f(path))) 80 | } 81 | 82 | pub fn link(target: &str, path: &str) { 83 | info!("{}", format!("linking {} to {}", general::path_f(path), general::path_f(target))) 84 | } 85 | 86 | pub fn remove_file(path: &str) { 87 | info!("{}", format!("removing {}", general::path_f(path))) 88 | } 89 | -------------------------------------------------------------------------------- /saku-lib/util/path.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use super::constants; 4 | 5 | use crate::prelude::*; 6 | use super::filepath; 7 | 8 | use glob::glob; 9 | 10 | pub fn exists(path: &str) -> bool { 11 | filepath::exists(path) 12 | } 13 | 14 | pub fn store_file() -> String { 15 | filepath::join(&constants::SAKU_DIR, &constants::STORE_NAME) 16 | } 17 | 18 | pub fn gr(name: &str) -> String { 19 | if name.len() == 0 { 20 | panic!("argument for group name was nil"); 21 | } 22 | filepath::join(&constants::PKG_DIR, name) 23 | } 24 | 25 | fn pkg_name(name: &str) -> String { 26 | if name.len() == 0 { 27 | panic!("argument for pkg name was nil"); 28 | } 29 | format!("{name}.fl") 30 | } 31 | 32 | pub fn remove_extension(name: String) -> String { 33 | if name.len() == 0 { 34 | panic!("argument for pkg name was nil") 35 | } 36 | 37 | let mut parts = name.split('.'); 38 | parts.next_back(); 39 | String::from(parts.collect::>().join(".")) 40 | } 41 | 42 | pub fn path_pkg(group: &str, name: &str) -> String { 43 | let gr = gr(group); 44 | filepath::join(&gr, &pkg_name(&name)) 45 | } 46 | 47 | pub fn pkg_exists(gr: &str, name: &str) -> bool { 48 | let p = path_pkg(gr, name); 49 | exists(&p) 50 | } 51 | 52 | pub fn path_determine(path: String) -> Result<(String, bool)> { 53 | let mut dir = false; 54 | let p: &str; 55 | 56 | if path.starts_with(".") || path.starts_with("/") { 57 | // path is either relative or absolute 58 | let path_bind = filepath::extend(&path)?; 59 | p = &path_bind; 60 | 61 | // path points to either a file or a directory 62 | if filepath::is_dir(p) { 63 | dir = true; 64 | } 65 | 66 | return Ok( (p.to_string(), dir) ); 67 | } 68 | 69 | // path is a name 70 | let path_bind = pkg_search(&path)?; 71 | p = &path_bind; 72 | 73 | Ok(( p.to_string(), false )) 74 | } 75 | 76 | pub fn pkg_search(name: &str) -> Result { 77 | if pkg_exists("core", name) { 78 | return Ok(path_pkg("core", name)); 79 | } 80 | 81 | for d in fs::read_dir(&*constants::PKG_DIR)? { 82 | let d = d?; 83 | let d_path_bind = d.path(); 84 | let d_path = match d_path_bind.to_str() { 85 | Some(s) => Ok(s), 86 | None => Err(Error::Unexpected), 87 | }?; 88 | if !filepath::is_dir(d_path) { 89 | continue 90 | } 91 | let gr_name = filepath::base_name(d_path)?; 92 | 93 | if pkg_exists(&gr_name, name) { 94 | return Ok(path_pkg(&gr_name, name)); 95 | } 96 | } 97 | 98 | Err(make_err!(NotFound, "pkg not found.")) 99 | } 100 | 101 | pub fn pkg_match(pattern: &str) -> Result> { 102 | let mut result = vec![]; 103 | let found_files = pkgs()?; 104 | let found: Vec<[String;2]> = found_files.iter().map(|s| [s[0].clone(), remove_extension(s[1].clone())]).collect(); 105 | drop(found_files); 106 | 107 | for i in 0..found.len() { 108 | let p = &found[i]; 109 | if p[1].contains(pattern) { 110 | result.push(p.clone()); 111 | } 112 | } 113 | 114 | Ok(result) 115 | } 116 | 117 | pub fn pkgs() -> Result> { 118 | let mut files = vec![]; 119 | 120 | debug!("searching for pkgs in {}", &*constants::PKG_DIR); 121 | for d in fs::read_dir(&*constants::PKG_DIR)? { 122 | let d = d?; 123 | let d_path_bind = d.path(); 124 | let d_path = match d_path_bind.to_str() { 125 | Some(s) => Ok(s), 126 | None => Err(Error::Unexpected), 127 | }?; 128 | let group = filepath::base_name(d_path)?; 129 | debug!("found pkg dir {}", &group); 130 | if group == *constants::FLASK_DIR_NAME { 131 | continue 132 | } 133 | for f in fs::read_dir(d_path)? { 134 | let f = f?; 135 | let f_path_bind = f.path(); 136 | let f_path = match f_path_bind.to_str() { 137 | Some(s) => Ok(s), 138 | None => Err(Error::Unexpected), 139 | }?; 140 | let name = filepath::base_name(f_path)?; 141 | files.push([ group.clone(), name.clone() ]); 142 | } 143 | } 144 | 145 | Ok(files) 146 | } 147 | 148 | pub fn repo_seed(path: &str) -> String { 149 | if path.len() == 0 { 150 | panic!("argument for path was nil") 151 | } 152 | filepath::join(path, &constants::REPO_SEED) 153 | } 154 | 155 | pub fn is_repo_seed(path: &Vec<&str>) -> bool { 156 | if let Some(last_part) = path.last() { 157 | if last_part == &*constants::REPO_SEED { 158 | return true 159 | } 160 | } 161 | return false 162 | } 163 | 164 | pub fn repo(name: &str) -> String { 165 | if name.len() == 0 { 166 | panic!("argument for repo name was nil") 167 | } 168 | filepath::join(&constants::REPO_DIR, name) 169 | } 170 | 171 | pub fn repo_exists(name: &str) -> bool { 172 | filepath::exists(&repo(name)) 173 | } 174 | 175 | pub fn repo_file(name: &str, path: &str) -> String { 176 | if path.len() == 0 { 177 | panic!("argument for file name was nil") 178 | } 179 | filepath::join(&repo(name), path) 180 | } 181 | 182 | pub fn root_dir(dir: &str) -> String { 183 | if dir.len() == 0 { 184 | panic!("argument for type dir was nil") 185 | } 186 | filepath::join(&constants::ROOT_DIR, dir) 187 | } 188 | 189 | pub fn root_file(dir: &str, path: &str) -> String { 190 | if path.len() == 0 { 191 | panic!("argument for path was nil") 192 | } 193 | let file = filepath::base_name(&path).unwrap(); 194 | if file == "." { 195 | panic!("argument for file was invalid") 196 | } 197 | filepath::join(&root_dir(dir), &file) 198 | } 199 | 200 | pub fn flask(name: &str) -> String { 201 | if name.len() == 0 { 202 | panic!("argument for flask name was nil") 203 | } 204 | filepath::join(&constants::FLASK_DIR, &pkg_name(name)) 205 | } 206 | 207 | pub fn flask_dir(name: &str) -> String { 208 | if name.len() == 0 { 209 | panic!("argument for flask name was nil") 210 | } 211 | filepath::join(&repo(name), &constants::FLASK_DIR_NAME) 212 | } 213 | 214 | pub fn flasks() -> Result> { 215 | let mut files = vec![]; 216 | 217 | for f in fs::read_dir(&*constants::FLASK_DIR)? { 218 | let f = f?; 219 | let f_path_bind = f.path(); 220 | let f_path = match f_path_bind.to_str() { 221 | Some(s) => Ok(s), 222 | None => Err(Error::Unexpected), 223 | }?; 224 | let name = filepath::base_name(f_path)?; 225 | files.push(name); 226 | } 227 | 228 | Ok(files) 229 | } 230 | 231 | pub fn get_store_dirs() -> Result> { 232 | let mut dirs = vec![]; 233 | for d in fs::read_dir(&*constants::STORE_DIR)? { 234 | let d = d?; 235 | let d_path_bind = d.path(); 236 | let d_path = match d_path_bind.to_str() { 237 | Some(s) => Ok(s), 238 | None => Err(Error::Unexpected), 239 | }?; 240 | let name = filepath::base_name(d_path)?; 241 | dirs.push(name); 242 | } 243 | Ok(dirs) 244 | } 245 | 246 | pub fn store_dir(name: &str) -> String { 247 | filepath::join(&*constants::STORE_DIR, name) 248 | } 249 | 250 | pub fn get_stored_files(name: &str) -> Result> { 251 | let mut files = vec![]; 252 | for d in fs::read_dir(store_dir(name))? { 253 | let d = d?; 254 | let d_path_bind = d.path(); 255 | let d_path = match d_path_bind.to_str() { 256 | Some(s) => Ok(s), 257 | None => Err(Error::Unexpected), 258 | }?; 259 | for entry in glob(&format!("{d_path}/**/*"))? { 260 | let name = entry?.to_str().unwrap().to_string(); 261 | let base_name = filepath::base_name(&name)?; 262 | if base_name.chars().nth(0) != Some('.') { 263 | files.push(name); 264 | } 265 | } 266 | } 267 | Ok(files) 268 | } 269 | 270 | pub fn get_stored_dirs(name: &str) -> Result> { 271 | let mut dirs = vec![]; 272 | for d in fs::read_dir(store_dir(name))? { 273 | let d = d?; 274 | let d_path_bind = d.path(); 275 | let d_path = match d_path_bind.to_str() { 276 | Some(s) => Ok(s), 277 | None => Err(Error::Unexpected), 278 | }?; 279 | dirs.push(d_path.to_string()); 280 | } 281 | Ok(dirs) 282 | } 283 | 284 | pub fn get_artifact_dirs(name: &str) -> Result> { 285 | let mut dirs = get_stored_dirs(name)?; 286 | for i in 0..dirs.len() { 287 | let p = dirs[i].clone(); 288 | if filepath::base_name(&p)? == "bin" { 289 | dirs.remove(i); 290 | return Ok(dirs); 291 | } 292 | } 293 | Ok(dirs) 294 | } 295 | 296 | pub fn get_stored_bin(name: &str) -> Result> { 297 | let mut files = vec![]; 298 | let store_path = store_dir(name); 299 | for entry in glob(&format!("{store_path}/bin/*"))? { 300 | files.push(entry?.to_str().unwrap().to_string()); 301 | } 302 | Ok(files) 303 | } 304 | -------------------------------------------------------------------------------- /saku-lib/util/url.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | use regex::Regex; 4 | 5 | pub fn extend_url(url: &str) -> Result { 6 | let mut extended = String::default(); 7 | 8 | let mut founddb = false; 9 | 10 | for s in url.chars().into_iter() { 11 | if s == '.' { 12 | extended = format!("https://{}", url); 13 | return Ok(extended); 14 | } 15 | 16 | if s == '/' { 17 | // found protocol 18 | if founddb { 19 | return Ok(url.to_string()); 20 | } 21 | 22 | // potential address already catched 23 | extended = format!("https://github.com/{}", url); 24 | return Ok(extended); 25 | } 26 | 27 | if s == ':' { 28 | founddb = true 29 | } 30 | } 31 | Ok(extended) 32 | } 33 | 34 | // changes url into human readable format 35 | // NOTE: this should not be used for networking 36 | pub fn shorten_url(url: &str) -> Result { 37 | // let long_url = format!("{url}"); 38 | // let mut parts = long_url.split('/'); 39 | // let last = parts 40 | // .nth_back(0) 41 | // .ok_or(make_err!(Missing, "url for flask is missing"))?; 42 | // let name = path::remove_extension(last.to_string()); 43 | 44 | let mut shortened = format!("{url}"); 45 | 46 | let protocol_re = Regex::new(r"\w+://")?; 47 | let mut parts: Vec<&str> = protocol_re.split(&shortened).collect(); 48 | if parts.len() > 1 { 49 | parts.remove(0); 50 | shortened = parts.join(""); 51 | } 52 | 53 | match shortened.strip_prefix("github.com/") { 54 | Some(s) => { 55 | shortened = s.to_string(); 56 | } 57 | None => {} 58 | }; 59 | match shortened.strip_suffix(".git") { 60 | Some(s) => { 61 | shortened = s.to_string(); 62 | } 63 | None => {} 64 | }; 65 | 66 | Ok(shortened) 67 | } 68 | 69 | // changes url into 70 | pub fn url_name(url: &str) -> Result { 71 | let mut name = shorten_url(url)?; 72 | 73 | name = name.replace("/", "."); 74 | 75 | Ok(name) 76 | } 77 | 78 | mod test { 79 | #[allow(unused_imports)] 80 | use crate::prelude::*; 81 | 82 | #[test] 83 | fn extend_url() -> Result<()> { 84 | assert_eq!( 85 | super::extend_url("comfysage/pkg")?, 86 | "https://github.com/comfysage/pkg" 87 | ); 88 | assert_eq!( 89 | super::extend_url("https://github.com/comfysage/pkg")?, 90 | "https://github.com/comfysage/pkg" 91 | ); 92 | assert_eq!( 93 | super::extend_url("aur.archlinux.org/pkg")?, 94 | "https://aur.archlinux.org/pkg" 95 | ); 96 | 97 | Ok(()) 98 | } 99 | 100 | #[test] 101 | fn shorten_url() -> Result<()> { 102 | assert_eq!( 103 | super::shorten_url("https://aur.archlinux.org/pkg.git")?, 104 | "aur.archlinux.org/pkg" 105 | ); 106 | assert_eq!( 107 | super::shorten_url("aur.archlinux.org/pkg.git")?, 108 | "aur.archlinux.org/pkg" 109 | ); 110 | assert_eq!( 111 | super::shorten_url("aur.archlinux.org/pkg")?, 112 | "aur.archlinux.org/pkg" 113 | ); 114 | 115 | assert_eq!( 116 | super::shorten_url("https://github.com/comfysage/pkg.git")?, 117 | "comfysage/pkg" 118 | ); 119 | assert_eq!( 120 | super::shorten_url("github.com/comfysage/pkg.git")?, 121 | "comfysage/pkg" 122 | ); 123 | assert_eq!( 124 | super::shorten_url("github.com/comfysage/pkg")?, 125 | "comfysage/pkg" 126 | ); 127 | assert_eq!( 128 | super::shorten_url("comfysage/pkg")?, 129 | "comfysage/pkg" 130 | ); 131 | Ok(()) 132 | } 133 | 134 | #[test] 135 | fn url_name() -> Result<()> { 136 | assert_eq!(super::url_name("comfysage/core")?, "comfysage.core"); 137 | assert_eq!( 138 | super::url_name("aur.archlinux.org/pkg")?, 139 | "aur.archlinux.org.pkg" 140 | ); 141 | Ok(()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /scripts/release/nightly/changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # show changes since the last version tag 4 | # --------------------------------------- 5 | git cliff -u 6 | -------------------------------------------------------------------------------- /scripts/release/nightly/create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # show changes since last release (ignoring nightly) and tag current HEAD with 4 | # nightly 5 | # ---------------------------------------------------------------------------- 6 | ./scripts/release/nightly/changelog.sh > CHANGELOG.md 7 | git reset 8 | git add CHANGELOG.md 9 | git commit -m 'chore(changelog): update nightly changelog' 10 | git tag -f nightly 11 | git push --tags --force 12 | 13 | branch=$(git branch --show-current) 14 | hash=$(git rev-parse HEAD | cut -c 1-7) 15 | 16 | # delete last nightly release and create new nightly release from changelog 17 | # create release with: 18 | # - *prerelease*: true 19 | # - *title*: branch-hash 20 | # - *tag*: nightly 21 | # - *branch|target*: nightly 22 | # ------------------------------------------------------------------------- 23 | gh release delete nightly -y 24 | gh release create -p nightly --target nightly -t "${branch}-${hash}" -F CHANGELOG.md 25 | -------------------------------------------------------------------------------- /scripts/release/nightly/release-name.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | branch=$(git branch --show-current) 4 | hash=$(git rev-parse HEAD | cut -c 1-7) 5 | 6 | echo "${branch}-${hash}" 7 | -------------------------------------------------------------------------------- /scripts/release/stable/changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # show changes since the last version tag and bump version 4 | # -------------------------------------------------------- 5 | git cliff -u --bump 6 | -------------------------------------------------------------------------------- /scripts/release/stable/create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | version="$(git cliff --bumped-version)" 4 | n_version="$(git cliff --bumped-version | sed -e 's/^v//')" 5 | 6 | # show changes since last release (ignoring nightly) and tag current HEAD with 7 | # bumped version 8 | # ---------------------------------------------------------------------------- 9 | ./scripts/release/stable/changelog.sh > CHANGELOG.md 10 | git reset 11 | git add CHANGELOG.md 12 | git commit -m "chore(changelog): create $version changelog" 13 | git tag -f "$version" 14 | git push --tags --force 15 | 16 | # delete last nightly release and create new nightly release from changelog 17 | # create release with: 18 | # - *title*: number of version 19 | # - *tag*: version 20 | # - *branch|target*: version 21 | # ------------------------------------------------------------------------- 22 | [[ $FORCE -gt 0 ]] && gh release delete "$n_version" -y 23 | gh release create "$n_version" -t "$version" -F CHANGELOG.md 24 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::builder::styling; 2 | use saku_lib as saku; 3 | use saku_cli as cli; 4 | use saku::pkg::{config, data}; 5 | use saku::pkg::pkg::Pkg; 6 | use saku::util::msg; 7 | use saku::prelude::*; 8 | 9 | use clap::{arg, Command}; 10 | 11 | fn get_commands() -> Command { 12 | let effects = (styling::Effects::BOLD | styling::Effects::UNDERLINE).clear(); 13 | let styles = styling::Styles::styled() 14 | .header(styling::AnsiColor::White.on_default() | effects) 15 | .usage(styling::AnsiColor::White.on_default() | effects) 16 | .literal(styling::AnsiColor::BrightWhite.on_default() | effects) 17 | .placeholder(styling::AnsiColor::BrightWhite.on_default() | effects); 18 | 19 | let version = env!("PKG_VERSION"); 20 | 21 | Command::new("saku") 22 | .about("a distro-independent package manager written in Rust.") 23 | .subcommand_required(true) 24 | .arg_required_else_help(true) 25 | .allow_external_subcommands(true) 26 | .styles(styles) 27 | .version(version) 28 | .subcommand(Command::new("env").about("Show environment script")) 29 | .subcommand( 30 | Command::new("config") 31 | .about("Manage global configuration") 32 | .subcommand_required(true) 33 | .arg_required_else_help(true) 34 | .subcommand(Command::new("init").about("Setup saku")) 35 | .subcommand(Command::new("create").about("Setup saku.toml")), 36 | ) 37 | .subcommand( 38 | Command::new("pkg") 39 | .about("Manage pkg configurations") 40 | .subcommand_required(true) 41 | .arg_required_else_help(true) 42 | .subcommand( 43 | Command::new("add") 44 | .about("Add pkg configuration") 45 | .arg_required_else_help(true) 46 | .args([ 47 | arg!( "Package to add"), 48 | arg!([URL] "Package url"), 49 | ]), 50 | ) 51 | .subcommand( 52 | Command::new("remove") 53 | .about("Remove pkg configuration") 54 | .arg_required_else_help(true) 55 | .arg(arg!( "Package to remove")), 56 | ) 57 | .subcommand( 58 | Command::new("show") 59 | .about("Show pkg configuration") 60 | .arg_required_else_help(true) 61 | .arg(arg!( "Package to show")), 62 | ), 63 | ) 64 | .subcommand( 65 | Command::new("add") 66 | .visible_alias("install") 67 | .about("Install a package") 68 | .arg_required_else_help(true) 69 | .arg(arg!( ... "Package to install")), 70 | ) 71 | .subcommand( 72 | Command::new("upgrade") 73 | .about("Upgrade a package") 74 | .arg_required_else_help(true) 75 | .arg(arg!( ... "Package to upgrade")), 76 | ) 77 | .subcommand( 78 | Command::new("remove") 79 | .visible_alias("uninstall") 80 | .about("Remove a package") 81 | .arg_required_else_help(true) 82 | .arg(arg!( ... "Package to remove")), 83 | ) 84 | .subcommand( 85 | Command::new("update") 86 | .about("Update flasks") 87 | .arg(arg!([NAME] ... "Flask to update")), 88 | ) 89 | .subcommand( 90 | Command::new("flask") 91 | .about("Add flasks") 92 | .arg(arg!([NAME] ... "Flask to add")), 93 | ) 94 | .subcommand( 95 | Command::new("show") 96 | .about("Show package details") 97 | .arg_required_else_help(true) 98 | .arg(arg!( ... "Package to show")), 99 | ) 100 | .subcommand( 101 | Command::new("search") 102 | .about("search for a package") 103 | .arg_required_else_help(true) 104 | .arg(arg!( ... "Pattern to search for")), 105 | ) 106 | .subcommand( 107 | Command::new("list") 108 | .about("List flasks") 109 | .arg( 110 | arg!(-i --installed ... "List installed packages") 111 | ) 112 | ) 113 | .subcommand( 114 | Command::new("task") 115 | .about("Run a task for a package") 116 | .subcommand_required(true) 117 | .arg_required_else_help(true) 118 | .subcommand( 119 | Command::new("clone") 120 | .about("Clone a package") 121 | .arg_required_else_help(true) 122 | .arg(arg!( ... "Package to clone")), 123 | ) 124 | .subcommand( 125 | Command::new("build") 126 | .about("Build a package") 127 | .arg_required_else_help(true) 128 | .arg(arg!( ... "Package to build")), 129 | ) 130 | .subcommand( 131 | Command::new("install") 132 | .about("Install a package") 133 | .arg_required_else_help(true) 134 | .arg(arg!( ... "Package to install")), 135 | ) 136 | ) 137 | .subcommand( 138 | Command::new("changelog") 139 | .about("show package changelog") 140 | .arg_required_else_help(false) 141 | .arg(arg!([NAME] "Package name")) 142 | ) 143 | } 144 | 145 | fn main() -> Result<()> { 146 | saku_logger::init(); 147 | let config = config::Config::new()?; 148 | 149 | let matches = get_commands().get_matches(); 150 | 151 | match matches.subcommand() { 152 | Some(("env", _)) => { 153 | cli::env::env()?; 154 | Ok(()) 155 | } 156 | Some(("config", sub_matches)) => { 157 | let subcommand = sub_matches.subcommand().unwrap_or(("init", sub_matches)); 158 | match subcommand { 159 | ("init", _) => cli::config::init(), 160 | ("create", _) => cli::config::create(), 161 | (&_, _) => Err(Error::Unexpected), 162 | } 163 | } 164 | Some(("pkg", sub_matches)) => { 165 | let subcommand = sub_matches.subcommand().unwrap_or(("show", sub_matches)); 166 | match subcommand { 167 | ("add", sub_matches) => { 168 | let name = sub_matches 169 | .get_one::("NAME") 170 | .ok_or(make_err!(Missing, "no package name specified."))?; 171 | let url = sub_matches.get_one::("URL"); 172 | 173 | msg::add(&name); 174 | let pkg = Pkg::new(&name, url.map_or("", |url| &url)); 175 | cli::add::add(&pkg)?; 176 | Ok(()) 177 | } 178 | ("remove", sub_matches) => { 179 | let name = sub_matches 180 | .get_one::("NAME") 181 | .ok_or(make_err!(Missing, "no package name specified."))?; 182 | cli::remove::remove(&name)?; 183 | Ok(()) 184 | } 185 | ("show", sub_matches) => { 186 | let package = sub_matches.get_one::("NAME"); 187 | println!("show pkg {package:?}"); 188 | Ok(()) 189 | } 190 | (&_, _) => Err(Error::Unexpected), 191 | } 192 | } 193 | Some(("add", sub_matches)) => { 194 | let paths = sub_matches 195 | .get_many::("NAME") 196 | .into_iter() 197 | .flatten() 198 | .collect::>(); 199 | if paths.len() < 1 { 200 | return Err(make_err!(Missing, "not enough arguments provided")); 201 | } 202 | paths 203 | .iter() 204 | .map(|name| cli::install::install(name)) 205 | .collect() 206 | } 207 | Some(("upgrade", sub_matches)) => { 208 | let paths = sub_matches 209 | .get_many::("NAME") 210 | .into_iter() 211 | .flatten() 212 | .collect::>(); 213 | if paths.len() < 1 { 214 | return Err(make_err!(Missing, "not enough arguments provided")); 215 | } 216 | paths 217 | .iter() 218 | .map(|name| cli::upgrade::upgrade(name)) 219 | .collect() 220 | } 221 | Some(("remove", sub_matches)) => { 222 | let paths = sub_matches 223 | .get_many::("NAME") 224 | .into_iter() 225 | .flatten() 226 | .collect::>(); 227 | if paths.len() < 1 { 228 | return Err(make_err!(Missing, "not enough arguments provided")); 229 | } 230 | paths 231 | .iter() 232 | .map(|name| cli::uninstall::remove(name)) 233 | .collect() 234 | } 235 | Some(("update", sub_matches)) => { 236 | let urls = sub_matches 237 | .get_many::("NAME") 238 | .into_iter() 239 | .flatten() 240 | .collect::>(); 241 | if urls.len() < 1 { 242 | cli::update::update()?; 243 | } else { 244 | urls.iter() 245 | .map(|url| cli::update::update_flask_from_url(url)) 246 | .collect::>()?; 247 | } 248 | if config.main.frozen_update { 249 | return Ok(()); 250 | } 251 | cli::upgrade::upgrade("saku")?; 252 | Ok(()) 253 | } 254 | Some(("flask", sub_matches)) => { 255 | let urls = sub_matches 256 | .get_many::("NAME") 257 | .into_iter() 258 | .flatten() 259 | .collect::>(); 260 | if urls.len() < 1 { 261 | // `saku flask` is currently an alias for `saku update` 262 | cli::update::update()?; 263 | } else { 264 | urls.iter() 265 | .map(|url| cli::flask::add(url)) 266 | .collect::>()?; 267 | } 268 | Ok(()) 269 | } 270 | Some(("show", sub_matches)) => { 271 | let paths = sub_matches 272 | .get_many::("NAME") 273 | .into_iter() 274 | .flatten() 275 | .collect::>(); 276 | if paths.len() < 1 { 277 | return Err(make_err!(Missing, "not enough arguments provided")); 278 | } 279 | paths.iter().map(|name| cli::show::show(name)).collect() 280 | } 281 | Some(("search", sub_matches)) => { 282 | let pattern = sub_matches.get_one::("NAME").ok_or(make_err!(Missing, "pattern missing"))?; 283 | saku_cli::search::search(&pattern)?; 284 | 285 | Ok(()) 286 | } 287 | Some(("list", sub_matches)) => { 288 | let flag = sub_matches.get_one::("installed").ok_or(make_err!())?; 289 | if *flag > 0 { 290 | saku_cli::list::list_installed()?; 291 | return Ok(()); 292 | } 293 | 294 | // list flasks 295 | saku_cli::list::list()?; 296 | 297 | Ok(()) 298 | } 299 | Some(("task", sub_matches)) => { 300 | let subcommand = sub_matches.subcommand().ok_or(make_err!(Missing, "subcommand missing"))?; 301 | match subcommand { 302 | ("clone", sub_matches) => { 303 | let name = sub_matches 304 | .get_one::("NAME") 305 | .ok_or(make_err!(Missing, "no package name specified."))?; 306 | 307 | let pkg = data::get_pkg(name)?; 308 | cli::install::clone_pkg(&pkg)?; 309 | 310 | Ok(()) 311 | } 312 | ("build", sub_matches) => { 313 | let name = sub_matches 314 | .get_one::("NAME") 315 | .ok_or(make_err!(Missing, "no package name specified."))?; 316 | 317 | let pkg = data::get_pkg(name)?; 318 | cli::install::build_pkg(&pkg)?; 319 | 320 | Ok(()) 321 | } 322 | ("install", sub_matches) => { 323 | let name = sub_matches 324 | .get_one::("NAME") 325 | .ok_or(make_err!(Missing, "no package name specified."))?; 326 | 327 | let pkg = data::get_pkg(name)?; 328 | pkg.install_root()?; 329 | 330 | Ok(()) 331 | } 332 | (&_, _) => Err(Error::Unexpected), 333 | } 334 | } 335 | Some(("changelog", sub_matches)) => { 336 | let name: String = sub_matches 337 | .get_one::("NAME").map_or("saku".to_string(), |v| v.to_owned()); 338 | 339 | cli::changelog::changelog(&name)?; 340 | Ok(()) 341 | } 342 | // If all subcommands are defined above, anything else is unreachable!() 343 | _ => { 344 | Err(make_err!(Missing, "missing command. run saku --help.")) 345 | }, 346 | } 347 | } 348 | --------------------------------------------------------------------------------