├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── bin └── devbox │ ├── cli.rs │ ├── commands │ ├── build.rs │ ├── completions.rs │ ├── doctor.rs │ ├── logs.rs │ ├── mod.rs │ ├── new.rs │ ├── ps.rs │ ├── start.rs │ ├── stop.rs │ ├── tasks.rs │ └── update.rs │ ├── main.rs │ └── prelude.rs ├── errors.rs ├── lib.rs ├── project.rs ├── service.rs └── task.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "ansi_term" 3 | version = "0.10.2" 4 | source = "registry+https://github.com/rust-lang/crates.io-index" 5 | 6 | [[package]] 7 | name = "arrayvec" 8 | version = "0.4.7" 9 | source = "registry+https://github.com/rust-lang/crates.io-index" 10 | dependencies = [ 11 | "nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", 12 | ] 13 | 14 | [[package]] 15 | name = "atty" 16 | version = "0.2.3" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | dependencies = [ 19 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 20 | "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", 21 | "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 22 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 23 | ] 24 | 25 | [[package]] 26 | name = "backtrace" 27 | version = "0.3.4" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | dependencies = [ 30 | "backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", 31 | "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 32 | "dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 33 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 34 | "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", 35 | "rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 36 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 37 | ] 38 | 39 | [[package]] 40 | name = "backtrace-sys" 41 | version = "0.1.16" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | dependencies = [ 44 | "cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 45 | "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", 46 | ] 47 | 48 | [[package]] 49 | name = "bitflags" 50 | version = "1.0.1" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | 53 | [[package]] 54 | name = "byteorder" 55 | version = "1.2.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | 58 | [[package]] 59 | name = "cc" 60 | version = "1.0.3" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | 63 | [[package]] 64 | name = "cfg-if" 65 | version = "0.1.2" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | 68 | [[package]] 69 | name = "clap" 70 | version = "2.29.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | dependencies = [ 73 | "ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", 74 | "atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 75 | "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 76 | "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 77 | "textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", 78 | "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 79 | "vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 80 | ] 81 | 82 | [[package]] 83 | name = "colored" 84 | version = "1.6.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | dependencies = [ 87 | "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 88 | ] 89 | 90 | [[package]] 91 | name = "crossbeam-deque" 92 | version = "0.2.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | dependencies = [ 95 | "crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 96 | "crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 97 | ] 98 | 99 | [[package]] 100 | name = "crossbeam-epoch" 101 | version = "0.3.1" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | dependencies = [ 104 | "arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", 105 | "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 106 | "crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 107 | "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 108 | "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 109 | "nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", 110 | "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 111 | ] 112 | 113 | [[package]] 114 | name = "crossbeam-utils" 115 | version = "0.2.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | dependencies = [ 118 | "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 119 | ] 120 | 121 | [[package]] 122 | name = "csv" 123 | version = "0.15.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | dependencies = [ 126 | "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 127 | "memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 128 | "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", 129 | ] 130 | 131 | [[package]] 132 | name = "dbghelp-sys" 133 | version = "0.2.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | dependencies = [ 136 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 137 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 138 | ] 139 | 140 | [[package]] 141 | name = "devbox" 142 | version = "0.1.0" 143 | dependencies = [ 144 | "clap 2.29.0 (registry+https://github.com/rust-lang/crates.io-index)", 145 | "colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 146 | "dirs 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 147 | "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 148 | "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 149 | "prettytable-rs 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", 150 | "rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 151 | "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", 152 | "serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", 153 | "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 154 | "toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", 155 | ] 156 | 157 | [[package]] 158 | name = "dirs" 159 | version = "1.0.2" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | dependencies = [ 162 | "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", 163 | "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 164 | ] 165 | 166 | [[package]] 167 | name = "either" 168 | version = "1.4.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | 171 | [[package]] 172 | name = "encode_unicode" 173 | version = "0.3.1" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | 176 | [[package]] 177 | name = "failure" 178 | version = "0.1.1" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | dependencies = [ 181 | "backtrace 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 182 | "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 183 | ] 184 | 185 | [[package]] 186 | name = "failure_derive" 187 | version = "0.1.1" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | dependencies = [ 190 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 191 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 192 | "synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", 193 | ] 194 | 195 | [[package]] 196 | name = "fuchsia-zircon" 197 | version = "0.3.3" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | dependencies = [ 200 | "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 201 | "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 202 | ] 203 | 204 | [[package]] 205 | name = "fuchsia-zircon-sys" 206 | version = "0.3.3" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | 209 | [[package]] 210 | name = "kernel32-sys" 211 | version = "0.2.2" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | dependencies = [ 214 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 215 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 216 | ] 217 | 218 | [[package]] 219 | name = "lazy_static" 220 | version = "0.2.11" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | 223 | [[package]] 224 | name = "lazy_static" 225 | version = "1.0.1" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | 228 | [[package]] 229 | name = "libc" 230 | version = "0.2.34" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | 233 | [[package]] 234 | name = "memchr" 235 | version = "1.0.2" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | dependencies = [ 238 | "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", 239 | ] 240 | 241 | [[package]] 242 | name = "memoffset" 243 | version = "0.2.1" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | 246 | [[package]] 247 | name = "nodrop" 248 | version = "0.1.12" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | 251 | [[package]] 252 | name = "num_cpus" 253 | version = "1.7.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | dependencies = [ 256 | "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", 257 | ] 258 | 259 | [[package]] 260 | name = "prettytable-rs" 261 | version = "0.6.7" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | dependencies = [ 264 | "atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 265 | "csv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", 266 | "encode_unicode 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 267 | "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 268 | "term 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 269 | "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 270 | ] 271 | 272 | [[package]] 273 | name = "quote" 274 | version = "0.3.15" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | 277 | [[package]] 278 | name = "rand" 279 | version = "0.4.2" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | dependencies = [ 282 | "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 283 | "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", 284 | "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 285 | ] 286 | 287 | [[package]] 288 | name = "rayon" 289 | version = "1.0.1" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | dependencies = [ 292 | "either 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 293 | "rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 294 | ] 295 | 296 | [[package]] 297 | name = "rayon-core" 298 | version = "1.4.0" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | dependencies = [ 301 | "crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 302 | "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 303 | "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", 304 | "num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 305 | "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 306 | ] 307 | 308 | [[package]] 309 | name = "redox_syscall" 310 | version = "0.1.32" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | 313 | [[package]] 314 | name = "redox_termios" 315 | version = "0.1.1" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | dependencies = [ 318 | "redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 319 | ] 320 | 321 | [[package]] 322 | name = "remove_dir_all" 323 | version = "0.5.0" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | dependencies = [ 326 | "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 327 | ] 328 | 329 | [[package]] 330 | name = "rustc-demangle" 331 | version = "0.1.5" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | 334 | [[package]] 335 | name = "rustc-serialize" 336 | version = "0.3.24" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | 339 | [[package]] 340 | name = "scopeguard" 341 | version = "0.3.3" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | 344 | [[package]] 345 | name = "serde" 346 | version = "1.0.24" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | 349 | [[package]] 350 | name = "serde_derive" 351 | version = "1.0.24" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | dependencies = [ 354 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 355 | "serde_derive_internals 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", 356 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 357 | ] 358 | 359 | [[package]] 360 | name = "serde_derive_internals" 361 | version = "0.18.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | dependencies = [ 364 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 365 | "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", 366 | ] 367 | 368 | [[package]] 369 | name = "strsim" 370 | version = "0.6.0" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | 373 | [[package]] 374 | name = "syn" 375 | version = "0.11.11" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | dependencies = [ 378 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 379 | "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", 380 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 381 | ] 382 | 383 | [[package]] 384 | name = "synom" 385 | version = "0.11.3" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | dependencies = [ 388 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 389 | ] 390 | 391 | [[package]] 392 | name = "synstructure" 393 | version = "0.6.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | dependencies = [ 396 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 397 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 398 | ] 399 | 400 | [[package]] 401 | name = "tempdir" 402 | version = "0.3.7" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | dependencies = [ 405 | "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 406 | "remove_dir_all 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 407 | ] 408 | 409 | [[package]] 410 | name = "term" 411 | version = "0.4.6" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | dependencies = [ 414 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 415 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 416 | ] 417 | 418 | [[package]] 419 | name = "termion" 420 | version = "1.5.1" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | dependencies = [ 423 | "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", 424 | "redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 425 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 426 | ] 427 | 428 | [[package]] 429 | name = "textwrap" 430 | version = "0.9.0" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | dependencies = [ 433 | "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 434 | ] 435 | 436 | [[package]] 437 | name = "toml" 438 | version = "0.4.5" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | dependencies = [ 441 | "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", 442 | ] 443 | 444 | [[package]] 445 | name = "unicode-width" 446 | version = "0.1.4" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | 449 | [[package]] 450 | name = "unicode-xid" 451 | version = "0.0.4" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | 454 | [[package]] 455 | name = "vec_map" 456 | version = "0.8.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | 459 | [[package]] 460 | name = "winapi" 461 | version = "0.2.8" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | 464 | [[package]] 465 | name = "winapi" 466 | version = "0.3.4" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | dependencies = [ 469 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 470 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 471 | ] 472 | 473 | [[package]] 474 | name = "winapi-build" 475 | version = "0.1.1" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | 478 | [[package]] 479 | name = "winapi-i686-pc-windows-gnu" 480 | version = "0.4.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | 483 | [[package]] 484 | name = "winapi-x86_64-pc-windows-gnu" 485 | version = "0.4.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | 488 | [metadata] 489 | "checksum ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455" 490 | "checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef" 491 | "checksum atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "21e50800ec991574876040fff8ee46b136a53e985286fbe6a3bdfe6421b78860" 492 | "checksum backtrace 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8709cc7ec06f6f0ae6c2c7e12f6ed41540781f72b488d83734978295ceae182e" 493 | "checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661" 494 | "checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" 495 | "checksum byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "652805b7e73fada9d85e9a6682a4abd490cb52d96aeecc12e33a0de34dfd0d23" 496 | "checksum cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a9b13a57efd6b30ecd6598ebdb302cca617930b5470647570468a65d12ef9719" 497 | "checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" 498 | "checksum clap 2.29.0 (registry+https://github.com/rust-lang/crates.io-index)" = "110d43e343eb29f4f51c1db31beb879d546db27998577e5715270a54bcf41d3f" 499 | "checksum colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b0aa3473e85a3161b59845d6096b289bb577874cafeaf75ea1b1beaa6572c7fc" 500 | "checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3" 501 | "checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150" 502 | "checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9" 503 | "checksum csv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7ef22b37c7a51c564a365892c012dc0271221fdcc64c69b19ba4d6fa8bd96d9c" 504 | "checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850" 505 | "checksum dirs 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "37a76dd8b997af7107d0bb69d43903cf37153a18266f8b3fdb9911f28efb5444" 506 | "checksum either 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "740178ddf48b1a9e878e6d6509a1442a2d42fd2928aae8e7a6f8a36fb01981b3" 507 | "checksum encode_unicode 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c088ec0ed2282dcd054f2c124c0327f953563e6c75fdc6ff5141779596289830" 508 | "checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82" 509 | "checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b" 510 | "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 511 | "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 512 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 513 | "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" 514 | "checksum lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e6412c5e2ad9584b0b8e979393122026cdd6d2a80b933f890dcd694ddbe73739" 515 | "checksum libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "36fbc8a8929c632868295d0178dd8f63fc423fd7537ad0738372bd010b3ac9b0" 516 | "checksum memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" 517 | "checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3" 518 | "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" 519 | "checksum num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "514f0d73e64be53ff320680ca671b64fe3fb91da01e1ae2ddc99eb51d453b20d" 520 | "checksum prettytable-rs 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "34dc1f4f6dddab3bf008ecfd4fd2a631b585fbf0af123f34c1324f51a034ff5f" 521 | "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 522 | "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" 523 | "checksum rayon 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80e811e76f1dbf68abf87a759083d34600017fc4e10b6bd5ad84a700f9dba4b1" 524 | "checksum rayon-core 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d24ad214285a7729b174ed6d3bcfcb80177807f959d95fafd5bfc5c4f201ac8" 525 | "checksum redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "ab105df655884ede59d45b7070c8a65002d921461ee813a024558ca16030eea0" 526 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 527 | "checksum remove_dir_all 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfc5b3ce5d5ea144bb04ebd093a9e14e9765bcfec866aecda9b6dec43b3d1e24" 528 | "checksum rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "aee45432acc62f7b9a108cc054142dac51f979e69e71ddce7d6fc7adf29e817e" 529 | "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" 530 | "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" 531 | "checksum serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1c57ab4ec5fa85d08aaf8ed9245899d9bbdd66768945b21113b84d5f595cb6a1" 532 | "checksum serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "02c92ea07b6e49b959c1481804ebc9bfd92d3c459f1274c9a9546829e42a66ce" 533 | "checksum serde_derive_internals 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75c6aac7b99801a16db5b40b7bf0d7e4ba16e76fbf231e32a4677f271cac0603" 534 | "checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" 535 | "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" 536 | "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" 537 | "checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd" 538 | "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 539 | "checksum term 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "fa63644f74ce96fbeb9b794f66aff2a52d601cbd5e80f4b97123e3899f4570f1" 540 | "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 541 | "checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" 542 | "checksum toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a7540f4ffc193e0d3c94121edb19b055670d369f77d5804db11ae053a45b6e7e" 543 | "checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" 544 | "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 545 | "checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" 546 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 547 | "checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" 548 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 549 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 550 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 551 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "devbox" 3 | version = "0.1.0" 4 | authors = ["Sonny Scroggin "] 5 | 6 | [dependencies] 7 | clap = "2" 8 | rayon = "1.0" 9 | serde = "1.0" 10 | serde_derive = "1.0" 11 | toml = "0.4.5" 12 | prettytable-rs = "0.6.7" 13 | failure = "0.1.1" 14 | failure_derive = "0.1.1" 15 | colored = "1.6.0" 16 | tempdir = "0.3.7" 17 | dirs = "1.0.2" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 2018 Sonny Scroggin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # devbox 2 | 3 | > devbox is responsible for bootstrapping your development environment. 4 | 5 | ## About devbox 6 | 7 | `devbox` is a work in progress. It is an opinionated wrapper around `docker` and 8 | `docker-compose`. 9 | 10 | `devbox` provides a way to get all of your services for local application 11 | development up and running quickly. Simply create a project and tell `devbox` 12 | what services are included, it will handle the rest. 13 | 14 | ## Installation 15 | 16 | In order to build this package you'll need a few dependencies install: 17 | 18 | * Rust 19 | * Docker 20 | 21 | ### Installing Rust 22 | 23 | The de-facto way to install [Rust](https://www.rust-lang.org/en-US/) is via [rustup](https://rustup.rs/). 24 | 25 | Run the following in your terminal, then follow the onscreen instructions. 26 | 27 | ```shell 28 | $ curl https://sh.rustup.rs -sSf | sh 29 | ``` 30 | 31 | ### Installing Docker 32 | 33 | If you're running macOS you'll want visit the [Docker for Mac website](https://store.docker.com/editions/community/docker-ce-desktop-mac) for 34 | installation instructions. 35 | 36 | ### Installing devbox 37 | 38 | To get `devbox` installed, simply run the following: 39 | 40 | ```shell 41 | $ cargo install --git https://github.com/scrogson/devbox --bin devbox 42 | ``` 43 | 44 | This will compile `devbox` and move the resulting executable into `~/.cargo/bin` 45 | which you should have been instructed to put in your `$PATH` when installing 46 | Rust. 47 | 48 | ### Generating a Project 49 | 50 | `devbox` works with projects. A project is a namespace to provide isolation 51 | between containers in other projects one might have. 52 | 53 | To generate a project, use the `new` subcommand with the name of our project like so: 54 | 55 | ```shell 56 | $ devbox new example 57 | ``` 58 | 59 | This will generate a couple of files in your home directory: 60 | 61 | ```shell 62 | $ tree ~/.config/devbox/ -L 2 63 | ~/.config/devbox/ 64 | └── example 65 | ├── config.toml 66 | └── docker-compose.yml 67 | 68 | 1 directory, 2 files 69 | ``` 70 | 71 | The `config.toml` file is the `devbox` configuration for your project. In it 72 | contains configuration for all of the services in your project. It looks like 73 | this: 74 | 75 | ```toml 76 | volumes = [ 77 | "mysql", 78 | "postgres" 79 | ] 80 | 81 | [services] 82 | example = { git = "git@github.com:user/example" } 83 | ``` 84 | 85 | #### Volumes 86 | 87 | Volumes is an array of `docker` volume names used in your project. These volumes 88 | will be created by `devbox build`. 89 | 90 | #### Services 91 | 92 | Services are declared by specifying the name of the service and configuring 93 | a `git` repository URL where the service can be cloned from. When working with 94 | a local service on disk, specify the `path` option along with the absolute path 95 | to the service on disk. 96 | 97 | ### Build the Docker Containers 98 | 99 | Set up the networking, pull down the latest docker images, and build the docker 100 | containers: 101 | 102 | ```shell 103 | $ devbox build -p example 104 | ``` 105 | 106 | ## Running devbox 107 | 108 | From the root of the repository: 109 | 110 | ```shell 111 | $ devbox start -p example 112 | ``` 113 | 114 | This will run the support services. 115 | 116 | The `ps` command can be used to list the running containers and confirm they have started correctly: 117 | 118 | ```shell 119 | λ devbox ps 120 | CONTAINER ID NAMES STATUS PORTS 121 | 8fd9e21e74dc example_kafka_1 Up 6 minutes 127.0.0.1:9092->9092/tcp 122 | 66520b2ba9cc example_zookeeper_1 Up 6 minutes 2888/tcp, 127.0.0.1:2181->2181/tcp, 3888/tcp 123 | f8f37e7baa69 example_mysql_1 Up 6 minutes 127.0.0.1:3306->3306/tcp 124 | ec04a7699e15 example_postgres_1 Up 6 minutes 127.0.0.1:5432->5432/tcp 125 | 45abb5cdd9ad example_elasticsearch_1 Up 6 minutes 127.0.0.1:9200->9200/tcp, 9300/tcp 126 | fe8b9d71bf70 example_redis_1 Up 6 minutes 127.0.0.1:6379->6379/tcp 127 | ``` 128 | 129 | ### Reading Logs 130 | 131 | Each service runs in its own docker container and writes its logs to standard 132 | output. In order to view the latest log output: 133 | 134 | ```shell 135 | $ devbox logs -p example mysql 136 | ``` 137 | 138 | To stream the logs in real time use the `--follow` flag: 139 | 140 | ```shell 141 | $ devbox logs -p example -f postgres 142 | ``` 143 | 144 | **Note:** use `devbox ps` to see a list of docker container names. 145 | 146 | ### Stopping devbox 147 | 148 | From the root of the repository: 149 | 150 | ```shell 151 | $ devbox stop -p example 152 | ``` 153 | 154 | This will stop the docker containers in the `example` project. 155 | 156 | ### DEVBOX_PROJECT 157 | 158 | Most of the time, you'll be using only a single devbox project at a time. 159 | Instead of explicitly passing `-p ` for each command, `devbox` can 160 | read your project name from the `DEVBOX_PROJECT` environment variable. 161 | 162 | ## Troubleshooting 163 | 164 | ### Failure Starting 165 | 166 | Ensure the Docker for Mac application is running. 167 | 168 | To always start on boot OS X users can go to `System Preferences > Users 169 | & Groups` and add the Docker application to the Login Items list. 170 | 171 | ### Services in Status Restarting (137) 172 | 173 | This is likely caused by Docker not having access to enough memory. You can 174 | change this in Docker preferences in the `Advanced` tab. By default Docker is 175 | set to request 2GB of memory. You may need to bump this to at least 4GB in order 176 | to run all services provided by `devbox`. 177 | -------------------------------------------------------------------------------- /src/bin/devbox/cli.rs: -------------------------------------------------------------------------------- 1 | use commands; 2 | use prelude::*; 3 | 4 | pub fn main() -> CliResult { 5 | let args = cli().get_matches(); 6 | execute_subcommand(&args) 7 | } 8 | 9 | fn execute_subcommand(args: &ArgMatches) -> CliResult { 10 | let (cmd, args) = match args.subcommand() { 11 | (cmd, Some(args)) => (cmd, args), 12 | _ => { 13 | cli().print_help()?; 14 | return Ok(()); 15 | } 16 | }; 17 | 18 | if let Some(exec) = commands::builtin_exec(cmd) { 19 | return exec(args); 20 | } 21 | 22 | Ok(()) 23 | } 24 | 25 | pub fn cli() -> App { 26 | App::new("devbox") 27 | .version(crate_version!()) 28 | .author(crate_authors!()) 29 | .setting(AppSettings::SubcommandRequiredElseHelp) 30 | .setting(AppSettings::VersionlessSubcommands) 31 | .setting(AppSettings::UnifiedHelpMessage) 32 | .setting(AppSettings::AllowExternalSubcommands) 33 | .about("Control your local infrastructure and services") 34 | .subcommands(commands::builtins()) 35 | } 36 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/build.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | use rayon::prelude::*; 3 | 4 | pub fn cli() -> App { 5 | subcommand("build") 6 | .about("Build infrastructure") 7 | .arg(Arg::with_name("SERVICE").help("The name of the service to build")) 8 | .arg(project()) 9 | } 10 | 11 | pub fn exec(args: &ArgMatches) -> CliResult { 12 | let mut project = args.project()?; 13 | 14 | if let Some(name) = args.value_of("SERVICE") { 15 | let mut service = project.find_service(name)?; 16 | let _ = service.clone_repo(); 17 | service.build() 18 | } else { 19 | let _ = create_network(&project); 20 | let _ = create_volumes(&project); 21 | let _ = pull_latest_images(); 22 | let _ = build_images(); 23 | let _ = clone_services(&mut project); 24 | let _ = build_services(&mut project); 25 | 26 | Ok(()) 27 | } 28 | } 29 | 30 | fn create_network(project: &Project) -> CliResult { 31 | println!("\nCreating '{}' network", &project.name); 32 | 33 | let _ = docker() 34 | .stdout(Stdio::null()) 35 | .stderr(Stdio::null()) 36 | .args(&["network", "create", &project.name]) 37 | .spawn()? 38 | .wait(); 39 | Ok(()) 40 | } 41 | 42 | fn create_volumes(project: &Project) -> CliResult { 43 | println!("\nCreating volumes"); 44 | project 45 | .volumes 46 | .par_iter() 47 | .map(|s| create_volume(s.as_str())) 48 | .collect::>(); 49 | Ok(()) 50 | } 51 | 52 | fn create_volume(name: &str) -> CliResult { 53 | println!("Creating volume: {}", name); 54 | let _ = docker() 55 | .stdout(Stdio::null()) 56 | .args(&["volume", "create", "--name", name]) 57 | .spawn()? 58 | .wait(); 59 | Ok(()) 60 | } 61 | 62 | fn pull_latest_images() -> CliResult { 63 | println!("\nPulling latest images..."); 64 | let _ = docker_compose().args(&["pull"]).spawn()?.wait(); 65 | Ok(()) 66 | } 67 | 68 | fn build_images() -> CliResult { 69 | println!("\nBuilding images..."); 70 | let _ = docker_compose().args(&["build"]).spawn()?.wait(); 71 | Ok(()) 72 | } 73 | 74 | fn clone_services(project: &mut Project) -> CliResult { 75 | project 76 | .services 77 | .par_iter_mut() 78 | .map(|ref mut service| service.clone_repo()) 79 | .collect::>(); 80 | Ok(()) 81 | } 82 | 83 | fn build_services(project: &mut Project) -> CliResult { 84 | project 85 | .services 86 | .par_iter_mut() 87 | .map(|ref mut service| service.build()) 88 | .collect::>(); 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/completions.rs: -------------------------------------------------------------------------------- 1 | use cli; 2 | use prelude::*; 3 | 4 | pub fn cli() -> App { 5 | subcommand("completions") 6 | .about("Generates completions for your shell") 7 | .arg( 8 | Arg::with_name("SHELL") 9 | .required(true) 10 | .possible_values(&["bash", "fish", "zsh"]) 11 | .help("The shell to generate the script for"), 12 | ) 13 | } 14 | 15 | pub fn exec(args: &ArgMatches) -> CliResult { 16 | let shell = args.value_of("SHELL") 17 | .ok_or_else(|| format_err!("Missing `SHELL` argument"))? 18 | .parse() 19 | .map_err(|err| format_err!("{}", err)) 20 | .context("Unable to parse `SHELL` argument")?; 21 | 22 | cli::cli().gen_completions_to("devbox", shell, &mut ::std::io::stdout()); 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/doctor.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | 3 | pub fn cli() -> App { 4 | subcommand("doctor").about("Check your system for potential problems") 5 | } 6 | 7 | pub fn exec(_args: &ArgMatches) -> CliResult { 8 | print_command_status("docker"); 9 | print_command_status("docker-compose"); 10 | 11 | Ok(()) 12 | } 13 | 14 | fn print_command_status(command: &str) { 15 | use colored::Colorize; 16 | 17 | if command_exists(command) { 18 | println!("{} {} was found in PATH", "✔".green(), command); 19 | } else { 20 | println!("{} {} was not found in PATH", "✘".red(), command); 21 | } 22 | } 23 | 24 | fn command_exists(command: &str) -> bool { 25 | match Command::new(command) 26 | .stdin(Stdio::null()) 27 | .stdout(Stdio::null()) 28 | .stderr(Stdio::null()) 29 | .spawn() 30 | { 31 | Ok(_) => true, 32 | Err(_) => false, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/logs.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | 3 | pub fn cli() -> App { 4 | subcommand("logs") 5 | .about("Display logs for running services") 6 | .arg( 7 | Arg::with_name("follow") 8 | .short("f") 9 | .long("follow") 10 | .help("Follow log output"), 11 | ) 12 | .arg( 13 | Arg::with_name("tail") 14 | .short("t") 15 | .long("tail") 16 | .takes_value(true) 17 | .default_value("10") 18 | .value_name("integer or \"all\"") 19 | .help("Number of lines to show from the end of the logs for each container."), 20 | ) 21 | .arg( 22 | Arg::with_name("SERVICE") 23 | .multiple(true) 24 | .value_name("SERVICE") 25 | .help("The name of the service(s) to log"), 26 | ) 27 | .arg(project()) 28 | } 29 | 30 | pub fn exec(args: &ArgMatches) -> CliResult { 31 | let mut project = args.project()?; 32 | 33 | let compose_file = &project.docker_compose_file.clone(); 34 | 35 | let mut cmd = Command::new("docker-compose"); 36 | 37 | // Set the devbox compose file 38 | cmd.arg("-f").arg(compose_file.clone()); 39 | 40 | if let Some(names) = args.values_of("SERVICE") { 41 | for name in names { 42 | maybe_append_docker_compose_override(&mut cmd, name, &mut project); 43 | } 44 | } 45 | 46 | cmd.arg("logs"); 47 | 48 | if args.is_present("follow") { 49 | cmd.arg("-f"); 50 | } 51 | 52 | if let Some(tail) = args.value_of("tail") { 53 | cmd.arg("--tail").arg(tail); 54 | } 55 | 56 | if let Some(names) = args.values_of("SERVICE") { 57 | names.for_each(|name| { 58 | cmd.arg(name); 59 | }); 60 | } 61 | 62 | let _ = cmd.spawn()?.wait(); 63 | 64 | Ok(()) 65 | } 66 | 67 | fn maybe_append_docker_compose_override<'a>( 68 | cmd: &'a mut Command, 69 | name: &str, 70 | project: &'a mut Project, 71 | ) -> &'a mut Command { 72 | match project.find_service(name) { 73 | Ok(ref mut service) => match service.devbox_compose_file().to_str() { 74 | Some(override_file) => cmd.args(&["-f", override_file]), 75 | None => cmd, 76 | }, 77 | Err(_) => cmd, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | 3 | pub fn builtins() -> Vec { 4 | vec![ 5 | build::cli(), 6 | completions::cli(), 7 | doctor::cli(), 8 | logs::cli(), 9 | new::cli(), 10 | ps::cli(), 11 | start::cli(), 12 | stop::cli(), 13 | tasks::cli(), 14 | update::cli(), 15 | ] 16 | } 17 | 18 | pub fn builtin_exec(cmd: &str) -> Option CliResult> { 19 | let f = match cmd { 20 | "build" => build::exec, 21 | "completions" => completions::exec, 22 | "doctor" => doctor::exec, 23 | "logs" => logs::exec, 24 | "new" => new::exec, 25 | "ps" => ps::exec, 26 | "start" => start::exec, 27 | "stop" => stop::exec, 28 | "tasks" => tasks::exec, 29 | "update" => update::exec, 30 | _ => return None, 31 | }; 32 | Some(f) 33 | } 34 | 35 | pub mod build; 36 | pub mod completions; 37 | pub mod doctor; 38 | pub mod logs; 39 | pub mod new; 40 | pub mod ps; 41 | pub mod start; 42 | pub mod stop; 43 | pub mod tasks; 44 | pub mod update; 45 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/new.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | 3 | pub fn cli() -> App { 4 | subcommand("new") 5 | .about("Generates a new devbox project template") 6 | .arg( 7 | Arg::with_name("PROJECT") 8 | .required(true) 9 | .help("The project name"), 10 | ) 11 | .arg( 12 | Arg::with_name("git") 13 | .long("git") 14 | .takes_value(true) 15 | .help("A URL to a git repository containing configuration for this project"), 16 | ) 17 | } 18 | 19 | pub fn exec(args: &ArgMatches) -> CliResult { 20 | let name = args.value_of("PROJECT") 21 | .ok_or_else(|| format_err!("Missing project name"))?; 22 | match args.value_of("git") { 23 | Some(repo) => Project::init_from_git(name, repo), 24 | None => Project::init(name), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/ps.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | 3 | pub fn cli() -> App { 4 | subcommand("ps").about("Display running services") 5 | } 6 | 7 | pub fn exec(_args: &ArgMatches) -> CliResult { 8 | let _ = docker() 9 | .args(&[ 10 | "ps", 11 | "--format", 12 | "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}", 13 | ]) 14 | .spawn()? 15 | .wait(); 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/start.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | 3 | pub fn cli() -> App { 4 | subcommand("start") 5 | .about("Start infrastructure or service") 6 | .arg(Arg::with_name("SERVICE").help("The name of the service to start")) 7 | .arg(project()) 8 | } 9 | 10 | pub fn exec(args: &ArgMatches) -> CliResult { 11 | let mut project = args.project()?; 12 | 13 | match args.value_of("SERVICE") { 14 | Some(name) => { 15 | let service = project.find_service(name)?; 16 | service.start() 17 | } 18 | None => { 19 | let _ = docker_compose().args(&["up", "-d"]).spawn()?.wait(); 20 | Ok(()) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/stop.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | use std::io::prelude::*; 3 | 4 | pub fn cli() -> App { 5 | subcommand("stop") 6 | .about("Stop infrastructure or service") 7 | .arg(Arg::with_name("SERVICE").help("The name of the service to stop")) 8 | .arg(project()) 9 | } 10 | 11 | pub fn exec(args: &ArgMatches) -> CliResult { 12 | let mut project = args.project()?; 13 | 14 | match args.value_of("SERVICE") { 15 | Some(name) => { 16 | let service = project.find_service(name)?; 17 | service.stop() 18 | } 19 | None => { 20 | let _ = destroy_environment(); 21 | let _ = remove_images(); 22 | Ok(()) 23 | } 24 | } 25 | } 26 | 27 | fn destroy_environment() -> CliResult { 28 | let _ = docker_compose() 29 | .args(&["down", "-v", "--remove-orphans"]) 30 | .spawn()? 31 | .wait(); 32 | Ok(()) 33 | } 34 | 35 | fn remove_images() -> CliResult { 36 | let images = docker() 37 | .args(&["images", "-q", "-f", "dangling=true"]) 38 | .output()?; 39 | 40 | let command = Command::new("xargs") 41 | .stdin(Stdio::piped()) 42 | .stdout(Stdio::piped()) 43 | .args(&["-I", "ARGS", "docker", "rmi", "-f", "ARGS"]) 44 | .spawn()?; 45 | 46 | command 47 | .stdin 48 | .ok_or_else(|| format_err!("Failed to read stdin"))? 49 | .write_all(&images.stdout)?; 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/tasks.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | 3 | pub fn cli() -> App { 4 | subcommand("tasks") 5 | .about("List and execute tasks for a service") 6 | .setting(AppSettings::SubcommandRequiredElseHelp) 7 | .subcommand( 8 | subcommand("list").about("List tasks for a service").arg( 9 | Arg::with_name("SERVICE") 10 | .required(true) 11 | .help("The name of the service"), 12 | ), 13 | ) 14 | .subcommand( 15 | subcommand("exec") 16 | .about("Execute tasks for an service") 17 | .arg( 18 | Arg::with_name("SERVICE") 19 | .required(true) 20 | .help("The name of the service"), 21 | ) 22 | .arg( 23 | Arg::with_name("TASKS") 24 | .required(true) 25 | .multiple(true) 26 | .help("The name(s) of the task(s)"), 27 | ), 28 | ) 29 | .arg(project()) 30 | } 31 | 32 | pub fn exec(matches: &ArgMatches) -> CliResult { 33 | let mut project = matches.project()?; 34 | 35 | let subcmd = matches 36 | .subcommand_name() 37 | .ok_or_else(|| format_err!("No subcommand found"))?; 38 | let args = matches 39 | .subcommand_matches(subcmd) 40 | .ok_or_else(|| format_err!("Error fetching argument for subcommand"))?; 41 | let service_name = args.value_of("SERVICE") 42 | .ok_or_else(|| format_err!("No `SERVICE` supplied"))?; 43 | let service = project.find_service(service_name)?; 44 | 45 | match subcmd { 46 | "list" => service.list_tasks(), 47 | "exec" => { 48 | let tasks = args.values_of_lossy("TASKS") 49 | .ok_or_else(|| format_err!("Error parsing `TASKS`"))?; 50 | service.exec_tasks(tasks) 51 | } 52 | _ => Ok(()), 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/bin/devbox/commands/update.rs: -------------------------------------------------------------------------------- 1 | use prelude::*; 2 | 3 | pub fn cli() -> App { 4 | subcommand("update") 5 | .about("Update a service") 6 | .arg( 7 | Arg::with_name("SERVICE") 8 | .required(true) 9 | .help("The name of the service to update"), 10 | ) 11 | .arg(project()) 12 | } 13 | 14 | pub fn exec(args: &ArgMatches) -> CliResult { 15 | let mut project = args.project()?; 16 | let name = args.value_of("SERVICE") 17 | .ok_or_else(|| format_err!("Error parsing `SERVICE`"))?; 18 | let service = project.find_service(name)?; 19 | service.update() 20 | } 21 | -------------------------------------------------------------------------------- /src/bin/devbox/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | extern crate colored; 4 | extern crate devbox; 5 | #[macro_use] 6 | extern crate failure; 7 | extern crate rayon; 8 | 9 | use devbox::*; 10 | 11 | mod cli; 12 | mod commands; 13 | mod prelude; 14 | 15 | fn run() -> Result<()> { 16 | cli::main() 17 | } 18 | 19 | fn main() { 20 | if let Err(ref err) = run() { 21 | use colored::*; 22 | 23 | eprintln!("{}", "ERROR:".red()); 24 | err.causes() 25 | .for_each(|cause| eprintln!("{}", format!("{}", cause).red())); 26 | 27 | std::process::exit(1); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/bin/devbox/prelude.rs: -------------------------------------------------------------------------------- 1 | use clap::{self, SubCommand}; 2 | pub use clap::{AppSettings, ArgMatches}; 3 | pub use devbox::{Project, Result}; 4 | pub use failure::ResultExt; 5 | pub use std::process::{Command, Stdio}; 6 | 7 | pub type App = clap::App<'static, 'static>; 8 | pub type Arg = clap::Arg<'static, 'static>; 9 | pub type CliResult = Result<()>; 10 | 11 | pub trait ArgMatchesExt { 12 | fn project(&self) -> Result { 13 | let name = self._value_of("PROJECT") 14 | .ok_or_else(|| format_err!("Project name required"))?; 15 | Project::new(name) 16 | } 17 | 18 | fn _value_of(&self, name: &str) -> Option<&str>; 19 | } 20 | 21 | impl<'a> ArgMatchesExt for ArgMatches<'a> { 22 | fn _value_of(&self, name: &str) -> Option<&str> { 23 | self.value_of(name) 24 | } 25 | } 26 | 27 | pub fn project() -> Arg { 28 | Arg::with_name("PROJECT") 29 | .help("Project name") 30 | .short("p") 31 | .long("project") 32 | .env("DEVBOX_PROJECT") 33 | .required(true) 34 | } 35 | 36 | pub fn subcommand(name: &'static str) -> App { 37 | SubCommand::with_name(name).settings(&[ 38 | AppSettings::UnifiedHelpMessage, 39 | AppSettings::DeriveDisplayOrder, 40 | AppSettings::DontCollapseArgsInUsage, 41 | ]) 42 | } 43 | 44 | pub fn docker() -> Command { 45 | Command::new("docker") 46 | } 47 | 48 | pub fn docker_compose() -> Command { 49 | Command::new("docker-compose") 50 | } 51 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use failure; 2 | 3 | pub type Error = failure::Error; 4 | pub type Result = ::std::result::Result; 5 | 6 | #[derive(Fail, Debug)] 7 | #[fail(display = "Unable to find service {}", _0)] 8 | pub struct ServiceNotFound(pub String); 9 | 10 | #[derive(Fail, Debug)] 11 | #[fail(display = "Unimplemented subcommand '{}'; please file a bug", _0)] 12 | pub struct UnimplementedSubcommand(pub String); 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | extern crate colored; 4 | extern crate dirs; 5 | #[macro_use] 6 | extern crate failure; 7 | #[macro_use] 8 | extern crate failure_derive; 9 | #[macro_use] 10 | extern crate prettytable; 11 | extern crate serde; 12 | #[macro_use] 13 | extern crate serde_derive; 14 | extern crate tempdir; 15 | extern crate toml; 16 | 17 | mod errors; 18 | mod project; 19 | mod service; 20 | mod task; 21 | 22 | pub use errors::*; 23 | pub use project::*; 24 | pub use service::*; 25 | -------------------------------------------------------------------------------- /src/project.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::{DirBuilder, File, OpenOptions}; 3 | use std::io::prelude::*; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::Command; 6 | 7 | use dirs::home_dir; 8 | use failure::ResultExt; 9 | use tempdir::TempDir; 10 | use toml; 11 | 12 | use errors::*; 13 | use service::Service; 14 | 15 | const TOML_TEMPLATE: &str = r#"# Example devbox project configuration 16 | # 17 | # Devbox can automatically create external docker volumes by specifying each 18 | # volume as a name. 19 | # 20 | # volumes = [ 21 | # "mysql", 22 | # "postgres" 23 | # ] 24 | # 25 | # Service definitions allow devbox to know how to clone or find the source code. 26 | # 27 | # * `name` - The name of the service 28 | # * `git` - The source of a git repository 29 | # * `path` - The path to the source on disk (optional) 30 | # 31 | # [services] 32 | # service1 = { git = "https://github.com/scrogson/service1" } 33 | # service2 = { path = "/path/to/service2" } 34 | "#; 35 | 36 | const COMPOSE_YAML_TEMPLATE: &str = r#"# This is an example docker-compose config file 37 | # 38 | # Replace the contents based on your project's requirements. 39 | # 40 | # See https://docs.docker.com/compose/compose-file for details. 41 | version: "3" 42 | 43 | networks: 44 | example: 45 | external: true 46 | 47 | volumes: 48 | postgres: 49 | external: true 50 | mysql: 51 | external: true 52 | 53 | services: 54 | 55 | redis: 56 | image: redis 57 | restart: "on-failure" 58 | ports: 59 | - "127.0.0.1:6379:6379" 60 | networks: 61 | - example 62 | 63 | mysql: 64 | image: mysql:5.6 65 | restart: "on-failure" 66 | environment: 67 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 68 | volumes: 69 | - mysql:/var/lib/mysql 70 | ports: 71 | - "127.0.0.1:3306:3306" 72 | networks: 73 | - example 74 | 75 | postgres: 76 | image: postgres:9.6 77 | restart: "on-failure" 78 | environment: 79 | POSTGRES_USER: "postgres" 80 | POSTGRES_PASSWORD: "postgres" 81 | PGDATA: /var/lib/postgresql/data/pgdata 82 | volumes: 83 | - postgres:/var/lib/postgresql/data/pgdata 84 | ports: 85 | - "127.0.0.1:5432:5432" 86 | networks: 87 | - example 88 | "#; 89 | 90 | #[derive(Debug)] 91 | pub struct Project { 92 | pub docker_compose_file: PathBuf, 93 | pub name: String, 94 | pub services: Vec, 95 | pub volumes: Vec, 96 | } 97 | 98 | impl Project { 99 | pub fn new(project_name: &str) -> Result { 100 | let toml_config_path = toml_config_path(project_name)?; 101 | let yaml_config_path = yaml_config_path(project_name)?; 102 | let value = parse_toml_config(toml_config_path)?; 103 | 104 | let services = match value.get("services") { 105 | Some(services) => services 106 | .as_table() 107 | .expect("services must be in table format") 108 | .iter() 109 | .map(|(name, attributes)| { 110 | let hooks = None; 111 | let name = name.to_owned(); 112 | let path = attributes 113 | .get("path") 114 | .map(|s| PathBuf::from(s.as_str().unwrap())); 115 | let project_name = project_name.to_owned(); 116 | let repo = attributes 117 | .get("git") 118 | .map(|s| s.as_str().unwrap().to_owned()); 119 | let tasks = None; 120 | 121 | Service { 122 | hooks, 123 | name, 124 | path, 125 | project_name, 126 | repo, 127 | tasks, 128 | } 129 | }) 130 | .collect(), 131 | None => Vec::new(), 132 | }; 133 | 134 | let volumes = match value.get("volumes") { 135 | Some(volumes) => volumes 136 | .as_array() 137 | .expect("volumes must be an array of strings") 138 | .to_vec() 139 | .iter() 140 | .map(|s| s.as_str().unwrap().to_string()) 141 | .collect(), 142 | None => Vec::new(), 143 | }; 144 | 145 | env::set_var("COMPOSE_PROJECT_NAME", &project_name); 146 | env::set_var("COMPOSE_FILE", &yaml_config_path); 147 | 148 | Ok(Project { 149 | docker_compose_file: yaml_config_path, 150 | name: project_name.to_owned(), 151 | services, 152 | volumes, 153 | }) 154 | } 155 | 156 | pub fn init(name: &str) -> Result<()> { 157 | let devbox_dir = devbox_dir(name)?; 158 | let toml_config = toml_config_path(name)?; 159 | let yaml_config = yaml_config_path(name)?; 160 | 161 | ensure_directory_exists(&devbox_dir); 162 | create_file_if_not_exists(&toml_config, TOML_TEMPLATE)?; 163 | create_file_if_not_exists(&yaml_config, COMPOSE_YAML_TEMPLATE)?; 164 | 165 | Ok(()) 166 | } 167 | 168 | pub fn init_from_git(name: &str, git: &str) -> Result<()> { 169 | let dir = TempDir::new("devbox")?; 170 | 171 | let _ = Command::new("git") 172 | .arg("clone") 173 | .arg(git) 174 | .arg(&dir.path()) 175 | .spawn()? 176 | .wait(); 177 | 178 | let toml_path = &dir.path().join("config.toml"); 179 | let yaml_path = &dir.path().join("docker-compose.yml"); 180 | let toml_contents = read_file(&toml_path)?; 181 | let yaml_contents = read_file(&yaml_path)?; 182 | 183 | ensure_directory_exists(&devbox_dir(name)?); 184 | create_file_if_not_exists(&toml_config_path(name)?, &toml_contents)?; 185 | create_file_if_not_exists(&yaml_config_path(name)?, &yaml_contents)?; 186 | 187 | dir.close()?; 188 | 189 | Ok(()) 190 | } 191 | 192 | pub fn find_service(&mut self, name: &str) -> Result<&mut Service> { 193 | let service = self.services 194 | .iter_mut() 195 | .find(|ref mut service| service.name == name) 196 | .ok_or_else(|| ServiceNotFound(name.to_owned()))?; 197 | 198 | let _ = service.rehydrate_from_devbox_toml(); 199 | 200 | Ok(service) 201 | } 202 | } 203 | 204 | fn parse_toml_config(path: PathBuf) -> Result { 205 | let mut contents = String::new(); 206 | 207 | File::open(path) 208 | .context("Couldn't find devbox project config")? 209 | .read_to_string(&mut contents) 210 | .context("Unable to read config file")?; 211 | 212 | let value = toml::from_str::(&contents)?; 213 | 214 | Ok(value) 215 | } 216 | 217 | pub fn devbox_dir(name: &str) -> Result { 218 | let home = home_dir().ok_or_else(|| format_err!("unable to determine home directory"))?; 219 | Ok(home.join(".config").join("devbox").join(name)) 220 | } 221 | 222 | fn read_file(path: &PathBuf) -> Result { 223 | let mut contents = String::new(); 224 | File::open(path)?.read_to_string(&mut contents)?; 225 | 226 | Ok(contents) 227 | } 228 | 229 | fn toml_config_path(name: &str) -> Result { 230 | Ok(devbox_dir(name)?.join("config.toml")) 231 | } 232 | 233 | fn yaml_config_path(name: &str) -> Result { 234 | Ok(devbox_dir(name)?.join("docker-compose.yml")) 235 | } 236 | 237 | fn ensure_directory_exists(path: &PathBuf) { 238 | if !path.exists() { 239 | println!("Creating {:?}", path); 240 | DirBuilder::new() 241 | .recursive(true) 242 | .create(path) 243 | .expect("Unable to create devbox project path"); 244 | } 245 | } 246 | 247 | fn create_file_if_not_exists(path: &Path, content: &str) -> Result<()> { 248 | if !&path.exists() { 249 | let mut file = OpenOptions::new().write(true).create_new(true).open(path)?; 250 | 251 | println!("Creating file {:?}", &path); 252 | 253 | file.write_all(content.as_bytes())?; 254 | } 255 | 256 | Ok(()) 257 | } 258 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::env; 3 | use std::ffi::OsStr; 4 | use std::fs::File; 5 | use std::io::prelude::*; 6 | use std::path::PathBuf; 7 | use std::process::Command; 8 | 9 | use colored::*; 10 | use failure::ResultExt; 11 | use prettytable::format; 12 | use prettytable::Table; 13 | use toml; 14 | 15 | use errors::*; 16 | use project; 17 | use task::Task; 18 | 19 | const COMPOSE_PATH: &str = ".devbox/docker-compose.yml"; 20 | const TOML_PATH: &str = ".devbox/config.toml"; 21 | 22 | pub fn cmd>(program: S, service: &Service) -> Command { 23 | let devbox_compose_file = env::var("COMPOSE_FILE").unwrap(); 24 | let service_compose_file = service.devbox_compose_file(); 25 | 26 | let mut cmd = Command::new(program); 27 | 28 | cmd.arg("-f") 29 | .arg(&devbox_compose_file) 30 | .arg("-f") 31 | .arg(&service_compose_file) 32 | .arg("--project-directory") 33 | .arg(&service_compose_file.parent().unwrap()); 34 | 35 | cmd 36 | } 37 | 38 | #[derive(Clone, Debug)] 39 | pub struct Service { 40 | pub hooks: Option>>, 41 | pub name: String, 42 | pub repo: Option, 43 | pub path: Option, 44 | pub project_name: String, 45 | pub tasks: Option>, 46 | } 47 | 48 | impl Service { 49 | pub fn rehydrate_from_devbox_toml(&mut self) -> Result<()> { 50 | let mut contents = String::new(); 51 | 52 | if let Ok(mut file) = File::open(self.devbox_toml_file()) { 53 | file.read_to_string(&mut contents) 54 | .context("Unable to read config file")?; 55 | } else { 56 | println!( 57 | "{} Config file not found, no tasks or hooks are defined for {}", 58 | "WARN".yellow(), 59 | self.name 60 | ); 61 | return Ok(()); 62 | } 63 | 64 | if let Ok(values) = toml::from_str(&contents) { 65 | let _ = self.insert_tasks(&values); 66 | let _ = self.insert_hooks(&values); 67 | } 68 | 69 | Ok(()) 70 | } 71 | 72 | pub fn start(&self) -> Result<()> { 73 | if self.devbox_compose_file().exists() { 74 | let _ = cmd("docker-compose", self) 75 | .arg("up") 76 | .arg("-d") 77 | .arg(&self.name) 78 | .spawn()? 79 | .wait(); 80 | 81 | return Ok(()); 82 | } 83 | 84 | Err(format_err!( 85 | "Failed to start {} - missing docker-compose file", 86 | self.name 87 | ))? 88 | } 89 | 90 | pub fn stop(&self) -> Result<()> { 91 | if self.devbox_compose_file().exists() { 92 | let _ = cmd("docker-compose", self) 93 | .arg("stop") 94 | .arg(&self.name) 95 | .spawn()? 96 | .wait(); 97 | 98 | return Ok(()); 99 | } 100 | 101 | Err(format_err!( 102 | "Failed to stop {} - missing docker-compose file", 103 | self.name 104 | ))? 105 | } 106 | 107 | pub fn build(&mut self) -> Result<()> { 108 | if self.devbox_compose_file().exists() { 109 | self.run_lifecycle_hooks("before-build")?; 110 | 111 | let _ = cmd("docker-compose", self) 112 | .arg("build") 113 | .arg(&self.name) 114 | .spawn()? 115 | .wait(); 116 | 117 | self.run_lifecycle_hooks("after-build")?; 118 | 119 | return Ok(()); 120 | } 121 | 122 | Err(format_err!( 123 | "Failed to build {} - missing docker-compose file", 124 | self.name 125 | ))? 126 | } 127 | 128 | pub fn path_exists(&self) -> bool { 129 | self.source_path().exists() 130 | } 131 | 132 | pub fn clone_repo(&self) -> Result<()> { 133 | if self.path_exists() { 134 | eprintln!("{} already exists, fetching updates...", self.name); 135 | self.update_repo() 136 | } else { 137 | match self.repo { 138 | Some(ref repo) => { 139 | let _ = Command::new("git") 140 | .arg("clone") 141 | .arg(repo) 142 | .arg(self.source_path()) 143 | .spawn()? 144 | .wait(); 145 | Ok(()) 146 | } 147 | None => Err(format_err!("No repository configured for {}", self.name)), 148 | } 149 | } 150 | } 151 | 152 | pub fn update(&mut self) -> Result<()> { 153 | if self.path_exists() { 154 | self.run_lifecycle_hooks("before-update")?; 155 | self.update_repo()?; 156 | self.run_lifecycle_hooks("after-update")?; 157 | 158 | Ok(()) 159 | } else { 160 | self.clone_repo() 161 | } 162 | } 163 | 164 | pub fn update_repo(&self) -> Result<()> { 165 | let _ = Command::new("git") 166 | .current_dir(self.source_path()) 167 | .args(&["pull", "origin", "master"]) 168 | .spawn()? 169 | .wait(); 170 | Ok(()) 171 | } 172 | 173 | pub fn find_task(&mut self, name: &str) -> Option { 174 | if let Ok(ref tasks) = self.tasks() { 175 | tasks.iter().cloned().find(|task| task.name == name) 176 | } else { 177 | None 178 | } 179 | } 180 | 181 | pub fn list_tasks(&mut self) -> Result<()> { 182 | let mut table = Table::new(); 183 | 184 | table.set_format(*format::consts::FORMAT_CLEAN); 185 | table.add_row(row!["TASK", "DESCRIPTION"]); 186 | 187 | if let Ok(ref tasks) = self.tasks() { 188 | for task in tasks { 189 | table.add_row(row![task.name, task.description]); 190 | } 191 | } 192 | 193 | table.printstd(); 194 | 195 | Ok(()) 196 | } 197 | 198 | pub fn exec_tasks(&mut self, task_names: Vec) -> Result<()> { 199 | for name in task_names { 200 | if let Some(ref task) = self.find_task(&name) { 201 | self.exec_task(task)?; 202 | } else { 203 | eprintln!( 204 | "Task '{}' could not be found for service {}", 205 | name, self.name 206 | ); 207 | } 208 | } 209 | 210 | Ok(()) 211 | } 212 | 213 | fn exec_task(&self, task: &Task) -> Result<()> { 214 | if self.devbox_compose_file().exists() { 215 | let _ = cmd("docker-compose", self) 216 | .arg("exec") 217 | .arg(&self.name) 218 | .args(&task.exec) 219 | .spawn()? 220 | .wait(); 221 | 222 | return Ok(()); 223 | } 224 | 225 | Err(format_err!( 226 | "Failed to execute {} - missing docker-compose file", 227 | self.name 228 | )) 229 | } 230 | 231 | fn run_task(&self, task: &Task) -> Result<()> { 232 | if self.devbox_compose_file().exists() { 233 | let _ = cmd("docker-compose", self) 234 | .arg("run") 235 | .arg("--rm") 236 | .arg(&self.name) 237 | .args(&task.exec) 238 | .spawn()? 239 | .wait(); 240 | 241 | return Ok(()); 242 | } 243 | 244 | Err(format_err!( 245 | "Failed to execute {} - missing docker-compose file", 246 | self.name 247 | )) 248 | } 249 | 250 | pub fn tasks(&mut self) -> Result> { 251 | match self.tasks { 252 | Some(ref v) => Ok(v.clone()), 253 | None => Ok(Vec::new()), 254 | } 255 | } 256 | 257 | fn hooks(&mut self) -> Result>> { 258 | match self.hooks { 259 | Some(ref v) => Ok(v.clone()), 260 | None => Ok(BTreeMap::new()), 261 | } 262 | } 263 | 264 | fn run_lifecycle_hooks(&mut self, lifecycle: &str) -> Result<()> { 265 | println!("{} Running {} hooks", "INFO".green(), lifecycle); 266 | let tasks = self.tasks_for_hook(lifecycle); 267 | if !tasks.is_empty() { 268 | for task in tasks { 269 | let _ = self.run_task(&task); 270 | } 271 | } 272 | 273 | Ok(()) 274 | } 275 | 276 | fn tasks_for_hook(&mut self, name: &str) -> Vec { 277 | self.hooks() 278 | .unwrap_or_default() 279 | .get(name) 280 | .unwrap_or(&vec![]) 281 | .to_vec() 282 | } 283 | 284 | fn insert_tasks(&mut self, values: &toml::Value) -> Result<()> { 285 | match values.get("tasks") { 286 | Some(v) => self.tasks = v.clone().try_into::>().ok(), 287 | None => println!("No tasks found for service '{}'", self.name), 288 | }; 289 | 290 | Ok(()) 291 | } 292 | 293 | fn insert_hooks(&mut self, values: &toml::Value) -> Result<()> { 294 | let hooks = match values.get("hooks") { 295 | Some(v) => { 296 | let mut hooks = BTreeMap::new(); 297 | let map = v.clone().try_into::>>()?; 298 | 299 | for (key, task_names) in &map { 300 | let mut tasks: Vec = Vec::new(); 301 | for name in task_names { 302 | match self.find_task(name.as_str()) { 303 | Some(task) => tasks.push(task), 304 | None => println!( 305 | "{} Task `{}` was not found in the available tasks", 306 | "WARN".yellow(), 307 | name.as_str() 308 | ), 309 | } 310 | } 311 | hooks.insert(key.clone(), tasks); 312 | } 313 | 314 | Some(hooks) 315 | } 316 | None => None, 317 | }; 318 | 319 | self.hooks = hooks; 320 | 321 | Ok(()) 322 | } 323 | 324 | pub fn source_path(&self) -> PathBuf { 325 | match self.path { 326 | Some(ref path) => path.into(), 327 | None => project::devbox_dir(&self.project_name) 328 | .expect("unable to determine devbox project directory") 329 | .join("src") 330 | .join(&self.name), 331 | } 332 | } 333 | 334 | pub fn devbox_compose_file(&self) -> PathBuf { 335 | match self.path { 336 | Some(ref path) => path.join(COMPOSE_PATH), 337 | None => project::devbox_dir(&self.project_name) 338 | .expect("unable to determine devbox project directory") 339 | .join("src") 340 | .join(&self.name) 341 | .join(COMPOSE_PATH), 342 | } 343 | } 344 | 345 | pub fn devbox_toml_file(&self) -> PathBuf { 346 | match self.path { 347 | Some(ref path) => path.join(TOML_PATH), 348 | None => project::devbox_dir(&self.project_name) 349 | .expect("unable to determine devbox project directory") 350 | .join("src") 351 | .join(&self.name) 352 | .join(TOML_PATH), 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/task.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, Deserialize)] 2 | pub struct Task { 3 | pub name: String, 4 | pub description: String, 5 | pub exec: Vec, 6 | } 7 | --------------------------------------------------------------------------------