├── .github └── dependabot.yml ├── .gitignore ├── AUTHORS ├── BENCH.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── distrobox-vs-unbox-benchmark.png └── toolbx-vs-unbox-benchmark.png ├── deny.toml ├── justfile ├── rustfmt.toml ├── src ├── config.rs ├── create.rs ├── lib.rs ├── list.rs ├── main.rs ├── namespaces.rs ├── remove.rs └── run.rs └── tests ├── create_alpine.rs ├── create_arch.rs ├── create_ubuntu.rs └── run_true.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | lopukhov 2 | -------------------------------------------------------------------------------- /BENCH.md: -------------------------------------------------------------------------------- 1 | 2 | Performance of the time it takes to enter into a toolbox using `hyperfine`. The benchmark may give 3 | a rough estimate of performance differences. 4 | 5 | # Toolbx vs unbox 6 | 7 | ![toolbx vs unbox](assets/toolbx-vs-unbox-benchmark.png) 8 | 9 | # Distrobox vs unbox 10 | 11 | ![distrobox vs unbox](assets/distrobox-vs-unbox-benchmark.png) 12 | 13 | # Nsbox vs unbox 14 | 15 | I was not able to get `nsbox` to work for this benchmark 16 | 17 | # Devbox vs unbox 18 | 19 | It could not be meaningfully compared because `devbox` does not have a `run` equivalent subcommand -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.19.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.1.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 25 | 26 | [[package]] 27 | name = "backtrace" 28 | version = "0.3.67" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" 31 | dependencies = [ 32 | "addr2line", 33 | "cc", 34 | "cfg-if", 35 | "libc", 36 | "miniz_oxide", 37 | "object", 38 | "rustc-demangle", 39 | ] 40 | 41 | [[package]] 42 | name = "bitflags" 43 | version = "1.3.2" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 46 | 47 | [[package]] 48 | name = "bytecount" 49 | version = "0.6.3" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" 52 | 53 | [[package]] 54 | name = "cc" 55 | version = "1.0.79" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 58 | 59 | [[package]] 60 | name = "cfg-if" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 64 | 65 | [[package]] 66 | name = "clap" 67 | version = "4.1.13" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "3c911b090850d79fc64fe9ea01e28e465f65e821e08813ced95bced72f7a8a9b" 70 | dependencies = [ 71 | "bitflags", 72 | "clap_derive", 73 | "clap_lex", 74 | "is-terminal", 75 | "once_cell", 76 | "strsim", 77 | "termcolor", 78 | ] 79 | 80 | [[package]] 81 | name = "clap_derive" 82 | version = "4.1.12" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "9a932373bab67b984c790ddf2c9ca295d8e3af3b7ef92de5a5bacdccdee4b09b" 85 | dependencies = [ 86 | "heck", 87 | "proc-macro2", 88 | "quote", 89 | "syn", 90 | ] 91 | 92 | [[package]] 93 | name = "clap_lex" 94 | version = "0.3.3" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" 97 | dependencies = [ 98 | "os_str_bytes", 99 | ] 100 | 101 | [[package]] 102 | name = "color-eyre" 103 | version = "0.6.2" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" 106 | dependencies = [ 107 | "backtrace", 108 | "eyre", 109 | "indenter", 110 | "once_cell", 111 | "owo-colors", 112 | ] 113 | 114 | [[package]] 115 | name = "console" 116 | version = "0.15.5" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" 119 | dependencies = [ 120 | "encode_unicode", 121 | "lazy_static", 122 | "libc", 123 | "unicode-width", 124 | "windows-sys 0.42.0", 125 | ] 126 | 127 | [[package]] 128 | name = "encode_unicode" 129 | version = "0.3.6" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 132 | 133 | [[package]] 134 | name = "errno" 135 | version = "0.2.8" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 138 | dependencies = [ 139 | "errno-dragonfly", 140 | "libc", 141 | "winapi", 142 | ] 143 | 144 | [[package]] 145 | name = "errno-dragonfly" 146 | version = "0.1.2" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 149 | dependencies = [ 150 | "cc", 151 | "libc", 152 | ] 153 | 154 | [[package]] 155 | name = "eyre" 156 | version = "0.6.8" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" 159 | dependencies = [ 160 | "indenter", 161 | "once_cell", 162 | ] 163 | 164 | [[package]] 165 | name = "filetime" 166 | version = "0.2.20" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" 169 | dependencies = [ 170 | "cfg-if", 171 | "libc", 172 | "redox_syscall", 173 | "windows-sys 0.45.0", 174 | ] 175 | 176 | [[package]] 177 | name = "fnv" 178 | version = "1.0.7" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 181 | 182 | [[package]] 183 | name = "gimli" 184 | version = "0.27.2" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" 187 | 188 | [[package]] 189 | name = "hashbrown" 190 | version = "0.12.3" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 193 | 194 | [[package]] 195 | name = "heck" 196 | version = "0.4.1" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 199 | 200 | [[package]] 201 | name = "hermit-abi" 202 | version = "0.3.1" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 205 | 206 | [[package]] 207 | name = "indenter" 208 | version = "0.3.3" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 211 | 212 | [[package]] 213 | name = "indexmap" 214 | version = "1.9.3" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 217 | dependencies = [ 218 | "autocfg", 219 | "hashbrown", 220 | ] 221 | 222 | [[package]] 223 | name = "indicatif" 224 | version = "0.17.3" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" 227 | dependencies = [ 228 | "console", 229 | "number_prefix", 230 | "portable-atomic", 231 | "unicode-width", 232 | ] 233 | 234 | [[package]] 235 | name = "io-lifetimes" 236 | version = "1.0.9" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" 239 | dependencies = [ 240 | "hermit-abi", 241 | "libc", 242 | "windows-sys 0.45.0", 243 | ] 244 | 245 | [[package]] 246 | name = "is-terminal" 247 | version = "0.4.5" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" 250 | dependencies = [ 251 | "hermit-abi", 252 | "io-lifetimes", 253 | "rustix", 254 | "windows-sys 0.45.0", 255 | ] 256 | 257 | [[package]] 258 | name = "lazy_static" 259 | version = "1.4.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 262 | 263 | [[package]] 264 | name = "libc" 265 | version = "0.2.140" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 268 | 269 | [[package]] 270 | name = "linux-raw-sys" 271 | version = "0.1.4" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 274 | 275 | [[package]] 276 | name = "log" 277 | version = "0.4.17" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 280 | dependencies = [ 281 | "cfg-if", 282 | ] 283 | 284 | [[package]] 285 | name = "memchr" 286 | version = "2.5.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 289 | 290 | [[package]] 291 | name = "memoffset" 292 | version = "0.7.1" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 295 | dependencies = [ 296 | "autocfg", 297 | ] 298 | 299 | [[package]] 300 | name = "miniz_oxide" 301 | version = "0.6.2" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 304 | dependencies = [ 305 | "adler", 306 | ] 307 | 308 | [[package]] 309 | name = "nix" 310 | version = "0.26.2" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" 313 | dependencies = [ 314 | "bitflags", 315 | "cfg-if", 316 | "libc", 317 | "memoffset", 318 | "pin-utils", 319 | "static_assertions", 320 | ] 321 | 322 | [[package]] 323 | name = "number_prefix" 324 | version = "0.4.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 327 | 328 | [[package]] 329 | name = "object" 330 | version = "0.30.3" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" 333 | dependencies = [ 334 | "memchr", 335 | ] 336 | 337 | [[package]] 338 | name = "once_cell" 339 | version = "1.17.1" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 342 | 343 | [[package]] 344 | name = "os_str_bytes" 345 | version = "6.5.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" 348 | 349 | [[package]] 350 | name = "owo-colors" 351 | version = "3.5.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 354 | 355 | [[package]] 356 | name = "papergrid" 357 | version = "0.7.1" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "1526bb6aa9f10ec339fb10360f22c57edf81d5678d0278e93bc12a47ffbe4b01" 360 | dependencies = [ 361 | "bytecount", 362 | "fnv", 363 | "unicode-width", 364 | ] 365 | 366 | [[package]] 367 | name = "pin-utils" 368 | version = "0.1.0" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 371 | 372 | [[package]] 373 | name = "portable-atomic" 374 | version = "0.3.19" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" 377 | 378 | [[package]] 379 | name = "proc-macro2" 380 | version = "1.0.53" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" 383 | dependencies = [ 384 | "unicode-ident", 385 | ] 386 | 387 | [[package]] 388 | name = "quote" 389 | version = "1.0.26" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 392 | dependencies = [ 393 | "proc-macro2", 394 | ] 395 | 396 | [[package]] 397 | name = "redox_syscall" 398 | version = "0.2.16" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 401 | dependencies = [ 402 | "bitflags", 403 | ] 404 | 405 | [[package]] 406 | name = "rustc-demangle" 407 | version = "0.1.22" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" 410 | 411 | [[package]] 412 | name = "rustix" 413 | version = "0.36.11" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" 416 | dependencies = [ 417 | "bitflags", 418 | "errno", 419 | "io-lifetimes", 420 | "libc", 421 | "linux-raw-sys", 422 | "windows-sys 0.45.0", 423 | ] 424 | 425 | [[package]] 426 | name = "same-file" 427 | version = "1.0.6" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 430 | dependencies = [ 431 | "winapi-util", 432 | ] 433 | 434 | [[package]] 435 | name = "serde" 436 | version = "1.0.158" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" 439 | dependencies = [ 440 | "serde_derive", 441 | ] 442 | 443 | [[package]] 444 | name = "serde_derive" 445 | version = "1.0.158" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" 448 | dependencies = [ 449 | "proc-macro2", 450 | "quote", 451 | "syn", 452 | ] 453 | 454 | [[package]] 455 | name = "serde_spanned" 456 | version = "0.6.1" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" 459 | dependencies = [ 460 | "serde", 461 | ] 462 | 463 | [[package]] 464 | name = "static_assertions" 465 | version = "1.1.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 468 | 469 | [[package]] 470 | name = "strsim" 471 | version = "0.10.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 474 | 475 | [[package]] 476 | name = "syn" 477 | version = "2.0.10" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40" 480 | dependencies = [ 481 | "proc-macro2", 482 | "quote", 483 | "unicode-ident", 484 | ] 485 | 486 | [[package]] 487 | name = "tabled" 488 | version = "0.10.0" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "56c3ee73732ffceaea7b8f6b719ce3bb17f253fa27461ffeaf568ebd0cdb4b85" 491 | dependencies = [ 492 | "papergrid", 493 | "unicode-width", 494 | ] 495 | 496 | [[package]] 497 | name = "tar" 498 | version = "0.4.38" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" 501 | dependencies = [ 502 | "filetime", 503 | "libc", 504 | "xattr", 505 | ] 506 | 507 | [[package]] 508 | name = "termcolor" 509 | version = "1.2.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 512 | dependencies = [ 513 | "winapi-util", 514 | ] 515 | 516 | [[package]] 517 | name = "toml" 518 | version = "0.7.3" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" 521 | dependencies = [ 522 | "serde", 523 | "serde_spanned", 524 | "toml_datetime", 525 | "toml_edit", 526 | ] 527 | 528 | [[package]] 529 | name = "toml_datetime" 530 | version = "0.6.1" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" 533 | dependencies = [ 534 | "serde", 535 | ] 536 | 537 | [[package]] 538 | name = "toml_edit" 539 | version = "0.19.8" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" 542 | dependencies = [ 543 | "indexmap", 544 | "serde", 545 | "serde_spanned", 546 | "toml_datetime", 547 | "winnow", 548 | ] 549 | 550 | [[package]] 551 | name = "unbox" 552 | version = "0.5.0" 553 | dependencies = [ 554 | "clap", 555 | "color-eyre", 556 | "indicatif", 557 | "nix", 558 | "serde", 559 | "tabled", 560 | "tar", 561 | "toml", 562 | "users", 563 | "walkdir", 564 | ] 565 | 566 | [[package]] 567 | name = "unicode-ident" 568 | version = "1.0.8" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 571 | 572 | [[package]] 573 | name = "unicode-width" 574 | version = "0.1.10" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 577 | 578 | [[package]] 579 | name = "users" 580 | version = "0.11.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" 583 | dependencies = [ 584 | "libc", 585 | "log", 586 | ] 587 | 588 | [[package]] 589 | name = "walkdir" 590 | version = "2.3.3" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" 593 | dependencies = [ 594 | "same-file", 595 | "winapi-util", 596 | ] 597 | 598 | [[package]] 599 | name = "winapi" 600 | version = "0.3.9" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 603 | dependencies = [ 604 | "winapi-i686-pc-windows-gnu", 605 | "winapi-x86_64-pc-windows-gnu", 606 | ] 607 | 608 | [[package]] 609 | name = "winapi-i686-pc-windows-gnu" 610 | version = "0.4.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 613 | 614 | [[package]] 615 | name = "winapi-util" 616 | version = "0.1.5" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 619 | dependencies = [ 620 | "winapi", 621 | ] 622 | 623 | [[package]] 624 | name = "winapi-x86_64-pc-windows-gnu" 625 | version = "0.4.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 628 | 629 | [[package]] 630 | name = "windows-sys" 631 | version = "0.42.0" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 634 | dependencies = [ 635 | "windows_aarch64_gnullvm", 636 | "windows_aarch64_msvc", 637 | "windows_i686_gnu", 638 | "windows_i686_msvc", 639 | "windows_x86_64_gnu", 640 | "windows_x86_64_gnullvm", 641 | "windows_x86_64_msvc", 642 | ] 643 | 644 | [[package]] 645 | name = "windows-sys" 646 | version = "0.45.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 649 | dependencies = [ 650 | "windows-targets", 651 | ] 652 | 653 | [[package]] 654 | name = "windows-targets" 655 | version = "0.42.2" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 658 | dependencies = [ 659 | "windows_aarch64_gnullvm", 660 | "windows_aarch64_msvc", 661 | "windows_i686_gnu", 662 | "windows_i686_msvc", 663 | "windows_x86_64_gnu", 664 | "windows_x86_64_gnullvm", 665 | "windows_x86_64_msvc", 666 | ] 667 | 668 | [[package]] 669 | name = "windows_aarch64_gnullvm" 670 | version = "0.42.2" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 673 | 674 | [[package]] 675 | name = "windows_aarch64_msvc" 676 | version = "0.42.2" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 679 | 680 | [[package]] 681 | name = "windows_i686_gnu" 682 | version = "0.42.2" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 685 | 686 | [[package]] 687 | name = "windows_i686_msvc" 688 | version = "0.42.2" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 691 | 692 | [[package]] 693 | name = "windows_x86_64_gnu" 694 | version = "0.42.2" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 697 | 698 | [[package]] 699 | name = "windows_x86_64_gnullvm" 700 | version = "0.42.2" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 703 | 704 | [[package]] 705 | name = "windows_x86_64_msvc" 706 | version = "0.42.2" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 709 | 710 | [[package]] 711 | name = "winnow" 712 | version = "0.4.1" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" 715 | dependencies = [ 716 | "memchr", 717 | ] 718 | 719 | [[package]] 720 | name = "xattr" 721 | version = "0.2.3" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" 724 | dependencies = [ 725 | "libc", 726 | ] 727 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unbox" 3 | version = "0.5.0" 4 | description = "Unshare a toolbox" 5 | license = "MPL-2.0" 6 | edition = "2021" 7 | readme = "README.md" 8 | repository = "https://github.com/lopukhov/unbox" 9 | keywords = ["containers", "toolbox", "unbox", "linux"] 10 | categories = ["virtualization"] 11 | 12 | [dependencies] 13 | clap = { version = "4", features = ["derive"] } 14 | color-eyre = { version = "0.6", default-features = false } 15 | indicatif = "0.17" 16 | nix = "0.26" 17 | serde = { version = "1", features = ["derive"] } 18 | tabled = { version = "0.10", default-features = false } 19 | tar = "0.4" 20 | toml = "0.7" 21 | users = "0.11" 22 | walkdir = "2" 23 | 24 | [profile.optimized] 25 | inherits = "release" 26 | lto = true 27 | strip = true 28 | codegen-units = 1 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unbox 2 | 3 | `unbox` or `unshare-toolbox` is an independent reimplementation of the ideas developed by [`toolbx`](https://containertoolbx.org/). 4 | It does _not_ use established container runtimes (`podman`, `docker`, `systemd-spawn`, etc) to isolate the processes, but instead it 5 | is implemented directly with Linux Namespaces to provide the environment of execution. For the creation of the images it is possible 6 | to use existing OCI images. 7 | 8 | > **Warning** 9 | > The implementation is still very young, and has not been "battle tested" at all, if you find any bugs, please open an issue. 10 | 11 | ## Installation 12 | 13 | There are no distro packages available yet, so the preferred way to install is to download the appropriate binary from the [releases page.](https://github.com/lopukhov/unbox/releases). 14 | 15 | It is necessary to have `newuidmap` and `newgidmap` already installed in your system (should probably be installed already) and your user should have subordinate users and groups configured 16 | in `/etc/subuid` and `/etc/subgid` with the following content: 17 | 18 | ``` 19 | :100000:65536 20 | ``` 21 | 22 | ### From source 23 | 24 | `unbox` can also be installed from source. You should install `Rust` and `cargo` first following [these instructions.](https://www.rust-lang.org/tools/install) 25 | 26 | If you have [`just`](https://github.com/casey/just) installed you can build using the `native` option for the most optimized experience: 27 | 28 | ```sh 29 | $ just native 30 | [...] 31 | 32 | $ cp ./target/optimized/unbox ~/.local/bin/ 33 | ``` 34 | 35 | Or a statically linked binary: 36 | 37 | ```sh 38 | $ just sbuild optimized 39 | [...] 40 | 41 | $ cp ./target/x86_64-unknown-linux-musl/optimized/unbox ~/.local/bin/ 42 | ``` 43 | 44 | But if you prefer a dynamically linked binary, you can also use: 45 | 46 | ```sh 47 | $ cargo build --profile optimized 48 | [...] 49 | 50 | $ cp ./target/optimized/unbox ~/.local/bin/ 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### Create 56 | 57 | The first step is to create a toolbox, which will store their root filesystems inside of `~/.local/share/unbox/images/`. 58 | 59 | If the rootfs is contained in a tarball it can be created from the following command: 60 | 61 | ```sh 62 | $ unbox create -t 63 | ``` 64 | 65 | If `podman` or `docker` are installed an OCI image can be downloaded and used, note that it may take a while if the image has not already been downloaded: 66 | 67 | ```sh 68 | $ unbox create -i -e 69 | ``` 70 | 71 | For example to create an Arch Linux toolbox from its official OCI image using `podman`: 72 | 73 | ```sh 74 | $ unbox create archlinux -i docker.io/archlinux:latest -e podman 75 | ``` 76 | 77 | In any case, it is possible to assign the default shell for the new image at creation time, in case the image does not have the current users' shell: 78 | 79 | ```sh 80 | $ unbox create alpine -i docker.io/alpine:latest -e podman -s /bin/sh 81 | ``` 82 | 83 | ### Enter 84 | 85 | To open an interactive shell inside an existing toolbox: 86 | 87 | ```sh 88 | $ unbox enter 89 | ``` 90 | 91 | ### Run 92 | 93 | To run a specific command inside an existing toolbox: 94 | 95 | ```sh 96 | $ unbox run -- 97 | ``` 98 | 99 | For example, in the previously created `archlinux` toolbox: 100 | 101 | ```sh 102 | $ unbox run archlinux -- ls -lh 103 | ``` 104 | 105 | ### List 106 | 107 | To list the names of the existing toolboxes: 108 | 109 | ```sh 110 | $ unbox list 111 | $ unbox ls 112 | ``` 113 | 114 | ### Remove 115 | 116 | To delete an existing toolbox: 117 | 118 | ```sh 119 | $ unbox rm 120 | $ unbox remove 121 | ``` 122 | 123 | Multiple toolboxes can be removed at the same time: 124 | 125 | ```sh 126 | $ unbox rm ... 127 | $ unbox remove ... 128 | ``` 129 | 130 | ### Configure 131 | 132 | Most toolboxes will have a configuration file stored at `~/.local/share/unbox/meta/`. To change the configuration of a toolbox the `configure` 133 | subcommand is recommended. To list the possible options: 134 | 135 | ```sh 136 | $ unbox cfg -h 137 | $ unbox configure --help 138 | ``` 139 | 140 | ## Alternatives 141 | 142 | There are a number of different implementations of the ideas originally developed by `toolbx`, this section compares `unbox` with each of them 143 | to flesh out their strengths and weaknesses. This comparison should not be regarded as absolute truth as it may be biased by my opinions and interests 144 | or lose accuracy with new developments in each of the different implementations. 145 | 146 | | Implementation | Language | Based on | Image | Time to enter | 147 | |------------------------------------------------------|----------|----------------------|------------------------|---------------| 148 | | [`toolbx`](https://github.com/containers/toolbox) | Go | `podman` | OCI images | 249 ms | 149 | | [`distrobox`](https://github.com/89luca89/distrobox) | Shell | `podman` or `docker` | OCI images | 153 ms | 150 | | [`nsbox`](https://github.com/refi64/nsbox) | Go | `systemd-nspawn` | Ansible | --- ms | 151 | | [`devbox`](https://github.com/jetpack-io/devbox) | Go | `nix-shell` | - | --- ms | 152 | | [`unbox`](https://github.com/lopukhov/unbox) | Rust | - | OCI images or tarballs | 2 ms | 153 | 154 | For the source of the "Time to enter" metric check [here](BENCH.md) 155 | 156 | ### Toolbx 157 | 158 | Toolbx is the original implementation and is the default in Fedora Silverblue. This means that it has been more "battle-tested" than the other alternatives, 159 | with more bugs being found and fixed and more features being considered implemented. Officially only Fedora images are supported, but it is possible to create 160 | images following their documentation for other distributions. In my opinion the most important downsides are the long time to enter into the toolbox and the lack 161 | of flexibility by using an OCI runtime. 162 | 163 | ### Distrobox 164 | 165 | Distrobox offers much more choice, from a wider set of tested images and a choice of using either `podman` or `docker` as the container manager. The project also 166 | has as one of its aims to be fast, and it is significantly faster than the original `toolbx` in my tests. It is probably the second most used implementation, so 167 | most features are already implemented and a lot of bugs have been fixed. In my opinion the most important downsides are the long time to enter into the toolbox 168 | (although one of their aims is to be as fast as possible the use of an OCI runtime puts a hard limit on how fast it can run), and the fact that is written in 169 | a shell language which I dislike for bigger projects. 170 | 171 | ### Devbox 172 | 173 | Devbox is a young implementation based on the `Nix` project, and it is more focused on giving reproducible environments to developers than on "pet" userspace for 174 | immutable distributions. In my opinion the biggest downside is the usage of `Nix`, which is not a pleasant experience to use in `ostree` distributions like 175 | Fedora Silverblue. Another downside that it shares with `unbox` is that because it is a younger project some features might be missing or some bugs may not have been 176 | found. 177 | 178 | ## License 179 | 180 | `unbox` is distributed under the Mozilla Public License v2, and any contributions will be incorporated under that license unless explicitly stated otherwise. 181 | -------------------------------------------------------------------------------- /assets/distrobox-vs-unbox-benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lopukhov/unbox/970134f73a71e0e2f368c0dfcc70d2cc291008e6/assets/distrobox-vs-unbox-benchmark.png -------------------------------------------------------------------------------- /assets/toolbx-vs-unbox-benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lopukhov/unbox/970134f73a71e0e2f368c0dfcc70d2cc291008e6/assets/toolbx-vs-unbox-benchmark.png -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | 2 | # If 1 or more target triples (and optionally, target_features) are specified, 3 | # only the specified targets will be checked when running `cargo deny check`. 4 | # This means, if a particular package is only ever used as a target specific 5 | # dependency, such as, for example, the `nix` crate only being used via the 6 | # `target_family = "unix"` configuration, that only having windows targets in 7 | # this list would mean the nix crate, as well as any of its exclusive 8 | # dependencies not shared by any other crates, would be ignored, as the target 9 | # list here is effectively saying which targets you are building for. 10 | targets = [] 11 | 12 | # This section is considered when running `cargo deny check advisories` 13 | # More documentation for the advisories section can be found here: 14 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 15 | [advisories] 16 | db-path = "~/.cargo/advisory-db" 17 | db-urls = ["https://github.com/rustsec/advisory-db"] 18 | vulnerability = "deny" 19 | unmaintained = "warn" 20 | yanked = "warn" 21 | notice = "warn" 22 | ignore = [] 23 | 24 | # This section is considered when running `cargo deny check licenses` 25 | # More documentation for the licenses section can be found here: 26 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 27 | [licenses] 28 | unlicensed = "deny" 29 | allow = [ 30 | "MIT", 31 | "Apache-2.0", 32 | "MPL-2.0", 33 | "Unicode-DFS-2016" 34 | ] 35 | deny = [] 36 | copyleft = "warn" 37 | allow-osi-fsf-free = "neither" 38 | default = "deny" 39 | confidence-threshold = 0.8 40 | exceptions = [] 41 | 42 | # Some crates don't have (easily) machine readable licensing information, 43 | # adding a clarification entry for it allows you to manually specify the 44 | # licensing information 45 | [[licenses.clarify]] 46 | name = "ring" 47 | version = "*" 48 | expression = "MIT AND ISC AND OpenSSL" 49 | license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] 50 | 51 | [licenses.private] 52 | ignore = false 53 | registries = [] 54 | 55 | # This section is considered when running `cargo deny check bans`. 56 | # More documentation about the 'bans' section can be found here: 57 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 58 | [bans] 59 | multiple-versions = "allow" 60 | wildcards = "allow" 61 | highlight = "all" 62 | allow = [] 63 | deny = [] 64 | skip = [] 65 | skip-tree = [] 66 | 67 | # This section is considered when running `cargo deny check sources`. 68 | # More documentation about the 'sources' section can be found here: 69 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 70 | [sources] 71 | unknown-registry = "warn" 72 | unknown-git = "warn" 73 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 74 | allow-git = [] 75 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: 2 | just --list 3 | 4 | # Check for incompatible licenses and security advisories, lint and run tests 5 | check: 6 | cargo deny check 7 | cargo clippy -q 8 | cargo run -q -- create -i docker.io/archlinux -e podman --quiet exec-tests 9 | cargo test -- --test-threads=1 10 | cargo run -q -- rm exec-tests 11 | 12 | # Profile the appropriate benchmark 13 | profile SUITE BENCH: 14 | cargo bench --bench {{SUITE}} -- --profile-time 60 {{BENCH}} 15 | 16 | # Dynamically build binary with selected PROFILE 17 | dbuild PROFILE: 18 | cargo build --profile {{PROFILE}} 19 | 20 | # Statically build the binary with the selected PROFILE (uses musl) 21 | sbuild PROFILE: 22 | RUSTFLAGS="-C target-feature=+crt-static -C link-self-contained=yes" cargo build --profile {{PROFILE}} --target x86_64-unknown-linux-musl 23 | 24 | # Build binary in optimized mode, with CPU native optimizations. Might make the binary incompatible for older CPUs 25 | native: 26 | RUSTFLAGS="-C target-cpu=native" cargo build --profile optimized 27 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::env; 6 | use std::ffi::OsString; 7 | use std::fs::{create_dir_all, File}; 8 | 9 | use clap::Args; 10 | use color_eyre::eyre; 11 | use color_eyre::eyre::WrapErr; 12 | use serde::{Deserialize, Serialize}; 13 | use toml::map::Keys; 14 | use toml::value::{Table, Value}; 15 | 16 | pub const STORAGE: &str = ".local/share/unbox"; 17 | 18 | /// Configure a toolbox creating a new meta-file if needed 19 | #[derive(Args, PartialEq, Eq, Debug)] 20 | pub struct Configure { 21 | #[clap(value_parser)] 22 | /// Name of the toolbox 23 | name: String, 24 | #[clap(short, long, value_parser)] 25 | /// Default shell for the image 26 | shell: Option, 27 | #[clap(short = 'n', long, value_parser)] 28 | /// Default hostname for the image 29 | hostname: Option, 30 | #[clap(long, value_parser)] 31 | /// Default home for the image 32 | home: Option, 33 | } 34 | 35 | pub fn configure(args: Configure) -> eyre::Result<()> { 36 | let mut config = 37 | Config::read_or_new(&args.name).wrap_err("Could not get configuration for the toolbox")?; 38 | if let Some(sh) = args.shell { 39 | config.shell = sh; 40 | } 41 | if let Some(host) = args.hostname { 42 | config.hostname = host; 43 | } 44 | if let Some(home) = args.home { 45 | config.home = home; 46 | } 47 | config.write(&args.name)?; 48 | Ok(()) 49 | } 50 | 51 | pub fn setup() -> eyre::Result<()> { 52 | use std::env::var; 53 | let home = var("HOME").wrap_err("Could not find current home")?; 54 | let meta = format!("{home}/{}/meta/", STORAGE); 55 | let images = format!("{home}/{}/images/", STORAGE); 56 | create_dir_all(meta).wrap_err("Could not create meta directory")?; 57 | create_dir_all(images).wrap_err("Could not create images directory") 58 | } 59 | 60 | #[derive(Debug, Deserialize, Serialize)] 61 | pub struct Config { 62 | pub shell: String, 63 | pub hostname: String, 64 | pub home: String, 65 | pub image: String, 66 | mounts: Table, 67 | } 68 | 69 | impl Config { 70 | pub fn new(name: &str) -> eyre::Result { 71 | use std::env::var; 72 | let shell = var("SHELL").wrap_err("Could not find current shell")?; 73 | let home = var("HOME").wrap_err("Could not find current home")?; 74 | let user = users::get_current_username() 75 | .expect("the user still exits") 76 | .into_string() 77 | .expect("Username is valid UTF8"); 78 | Ok(Config { 79 | shell, 80 | hostname: name.to_string(), 81 | home: format!("/home/{user}"), 82 | image: format!("{home}/{}/images/{name}", STORAGE), 83 | mounts: Config::default_mounts(), 84 | }) 85 | } 86 | 87 | pub fn read(name: &str) -> eyre::Result { 88 | let home = env::var("HOME").wrap_err("Could not find current home")?; 89 | let storage = format!("{home}/{}/meta/{name}.toml", STORAGE); 90 | let meta = std::fs::read_to_string(storage).wrap_err("Could not read meta file")?; 91 | let config: Config = toml::from_str(&meta).wrap_err("Meta file is corrupted")?; 92 | Ok(config) 93 | } 94 | 95 | pub fn read_or_new(name: &str) -> eyre::Result { 96 | match Config::read(name) { 97 | Ok(config) => Ok(config), 98 | Err(_) => Config::new(name), 99 | } 100 | } 101 | 102 | pub fn write(&self, name: &str) -> eyre::Result<()> { 103 | use std::io::prelude::*; 104 | let home = env::var("HOME").wrap_err("Could not find current home")?; 105 | let storage = format!("{home}/{}/meta/{name}.toml", STORAGE); 106 | let content = toml::to_string(self).expect("valid toml config"); 107 | let mut file = File::create(storage).wrap_err("Could not create meta file")?; 108 | file.write_all(content.as_bytes())?; 109 | Ok(()) 110 | } 111 | 112 | pub fn mounts(&self) -> Mounts<'_> { 113 | Mounts { 114 | keys: self.mounts.keys(), 115 | table: &self.mounts, 116 | } 117 | } 118 | 119 | fn default_mounts() -> Table { 120 | [ 121 | ("/proc", "/host/proc"), 122 | ("/sys", "/host/sys"), 123 | ("/tmp", "/host/tmp"), 124 | ("/dev", "/host/dev"), 125 | ("/run", "/host/run"), 126 | ("/home", "/host/home"), 127 | ("/etc/hosts", "/host/etc/hosts"), 128 | ("/etc/resolv.conf", "/host/etc/resolv.conf"), 129 | ] 130 | .into_iter() 131 | .map(|(dst, src)| (dst.into(), Value::String(src.into()))) 132 | .collect() 133 | } 134 | } 135 | 136 | pub struct Mounts<'a> { 137 | keys: Keys<'a>, 138 | table: &'a Table, 139 | } 140 | 141 | impl Iterator for Mounts<'_> { 142 | type Item = eyre::Result; 143 | fn next(&mut self) -> Option { 144 | if let Some(key) = self.keys.next() { 145 | let val = self.table.get(key).unwrap(); 146 | let source = match val { 147 | Value::String(source) => source.into(), 148 | _ => return Some(Err(eyre::eyre!("Invalid mount info entry"))), 149 | }; 150 | Some(Ok(MountInfo { 151 | source, 152 | target: key.into(), 153 | })) 154 | } else { 155 | None 156 | } 157 | } 158 | } 159 | 160 | pub struct MountInfo { 161 | pub source: OsString, 162 | pub target: OsString, 163 | } 164 | 165 | impl From<(&str, &str)> for MountInfo { 166 | fn from(info: (&str, &str)) -> Self { 167 | MountInfo { 168 | source: info.0.into(), 169 | target: info.1.into(), 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/create.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::ffi::OsStr; 6 | use std::fmt::Display; 7 | use std::fs::File; 8 | use std::path::{Path, PathBuf}; 9 | use std::process::Output; 10 | use std::time::Duration; 11 | 12 | use clap::{Args, ValueEnum}; 13 | use color_eyre::eyre; 14 | use color_eyre::eyre::WrapErr; 15 | use indicatif::ProgressBar; 16 | use nix::sched::CloneFlags; 17 | use std::fs::create_dir_all; 18 | use tar::Archive; 19 | 20 | use crate::config::Config; 21 | use crate::namespaces::{Mapping, Namespace}; 22 | 23 | /// Create a toolbox rootfs from an image 24 | #[derive(Args, PartialEq, Eq, Debug)] 25 | pub struct Create { 26 | #[clap(value_parser)] 27 | /// Name of the toolbox 28 | pub name: String, 29 | #[clap(short, long, value_parser)] 30 | /// Path to the tarball 31 | pub tar: Option, 32 | #[clap(short, long, value_parser)] 33 | /// Url of the OCI image 34 | pub image: Option, 35 | #[clap(short, long, value_parser)] 36 | /// OCI engine to extract the rootfs 37 | pub engine: Option, 38 | #[clap(short, long, value_parser)] 39 | /// Default shell for the image to be created 40 | pub shell: Option, 41 | #[clap(short, long, value_parser)] 42 | /// Default shell for the image to be created 43 | pub quiet: bool, 44 | } 45 | 46 | /// OCI engine to extract the rootfs (docker or podman) 47 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, ValueEnum)] 48 | pub enum Engine { 49 | Docker, 50 | Podman, 51 | } 52 | 53 | pub fn create(args: Create) -> eyre::Result<()> { 54 | let mut config = Config::new(&args.name)?; 55 | let new_root = &config.image; 56 | eyre::ensure!( 57 | !Path::new(new_root).exists(), 58 | "There is already an image with that name" 59 | ); 60 | create_dir_all(new_root).wrap_err("Could not create the new root directory")?; 61 | if let Some(sh) = args.shell { 62 | config.shell = sh; 63 | } 64 | config.write(&args.name)?; 65 | 66 | if let Some(tar) = args.tar { 67 | setup_new_root(new_root, tar, args.quiet) 68 | } else if let Some(oci) = args.image { 69 | // podman export $(podman create alpine) --output=alpine.tar 70 | let tar_file = format!("/tmp/unbox-{}-image.tar", args.name); 71 | match args 72 | .engine 73 | .ok_or_else(|| eyre::eyre!("A valid engine has not been provided"))? 74 | { 75 | Engine::Docker => get_image("docker", &oci, &tar_file, args.quiet)?, 76 | Engine::Podman => get_image("podman", &oci, &tar_file, args.quiet)?, 77 | }; 78 | setup_new_root(new_root, tar_file.into(), args.quiet) 79 | } else { 80 | Err(eyre::eyre!( 81 | "No tar archive or valid OCI arguments have been provided" 82 | )) 83 | } 84 | } 85 | 86 | struct Spinner(Option); 87 | 88 | impl Spinner { 89 | fn new(quiet: bool) -> Self { 90 | use indicatif::ProgressStyle; 91 | 92 | if quiet { 93 | Spinner(None) 94 | } else { 95 | let style = ProgressStyle::default_spinner() 96 | .template("{msg} {spinner}") 97 | .expect("valid template"); 98 | let spinner = ProgressBar::new_spinner().with_style(style); 99 | spinner.enable_steady_tick(Duration::from_millis(50)); 100 | Spinner(Some(spinner)) 101 | } 102 | } 103 | 104 | fn message(&self, msg: &'static str) { 105 | if let Some(spinner) = &self.0 { 106 | spinner.set_message(msg); 107 | } 108 | } 109 | 110 | // TODO: Drop 111 | fn clear(&self) { 112 | if let Some(spinner) = &self.0 { 113 | spinner.finish_and_clear(); 114 | } 115 | } 116 | } 117 | 118 | fn setup_new_root(new_root: &str, tar: PathBuf, quiet: bool) -> eyre::Result<()> { 119 | let flags = CloneFlags::CLONE_NEWUSER; 120 | let uid = users::get_current_uid().to_string(); 121 | let gid = users::get_current_gid().to_string(); 122 | let mut ns = Namespace::start(flags, &[id_map(&uid)], &[id_map(&gid)])?; 123 | ns.wait(); 124 | let spinner = Spinner::new(quiet); 125 | spinner.message("Unpacking tar file"); 126 | unpack_tar(tar, new_root)?; 127 | spinner.message("Setting up files and directories"); 128 | let dirs = ["host", "proc", "sys", "dev"]; 129 | create_dirs(new_root, &dirs)?; 130 | File::create(format!("{new_root}/etc/resolv.conf")).expect("path exists and is writable"); 131 | // TODO: create user 132 | spinner.clear(); 133 | Ok(()) 134 | } 135 | 136 | fn unpack_tar(tar: PathBuf, new_root: &str) -> eyre::Result<()> { 137 | let archive = File::open(tar).wrap_err("Could not open the tar file")?; 138 | let mut tar = Archive::new(archive); 139 | let mut dirs = Vec::new(); 140 | for entry in tar.entries()? { 141 | let mut entry = entry?; 142 | let path = entry.path()?; 143 | if path.is_dir() { 144 | dirs.push(entry); 145 | } else { 146 | entry 147 | .unpack_in(new_root) 148 | .wrap_err("Could not unpack entry")?; 149 | } 150 | } 151 | dirs.sort_unstable_by_key(|b| std::cmp::Reverse(b.path_bytes().len())); 152 | for mut dir in dirs { 153 | dir.unpack_in(new_root) 154 | .wrap_err("Could not unpack a directory")?; 155 | } 156 | Ok(()) 157 | } 158 | 159 | fn get_image(engine: &str, url: &str, tar_file: &str, quiet: bool) -> eyre::Result<()> { 160 | let spinner = Spinner::new(quiet); 161 | spinner.message("Downloading image"); 162 | let cid = spawn(engine, &["create", url])?.stdout; 163 | let cid = std::str::from_utf8(&cid) 164 | .expect("Podman/Docker gives valid utf8 output") 165 | .trim(); 166 | spawn(engine, &["export", cid, "--output", tar_file])?; 167 | spawn(engine, &["rm", cid])?; 168 | spinner.clear(); 169 | Ok(()) 170 | } 171 | 172 | fn spawn(cmd: S, args: &[S]) -> eyre::Result 173 | where 174 | S: AsRef, 175 | S: Display, 176 | { 177 | use std::process::Command; 178 | Command::new(cmd) 179 | .args(args) 180 | .output() 181 | .wrap_err("Could not execute the provided engine") 182 | } 183 | 184 | fn create_dirs(root: &str, dirs: &[&str]) -> eyre::Result<()> { 185 | for dir in dirs { 186 | create_dir_all(format!("{root}/{dir}")).expect("path exists and is writable"); 187 | } 188 | Ok(()) 189 | } 190 | 191 | fn id_map(guid: &str) -> Mapping<'_> { 192 | Mapping { 193 | inside: "0", 194 | outside: guid, 195 | len: "1", 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #![forbid(unsafe_code)] 6 | #![warn(rust_2018_idioms)] 7 | 8 | // TODO: add documentation 9 | 10 | pub mod config; 11 | pub mod create; 12 | pub mod list; 13 | pub mod namespaces; 14 | pub mod remove; 15 | pub mod run; 16 | -------------------------------------------------------------------------------- /src/list.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::{borrow::Cow, env}; 6 | 7 | use clap::Args; 8 | use color_eyre::eyre; 9 | use color_eyre::eyre::WrapErr; 10 | use tabled::{Style, Table, Tabled}; 11 | 12 | use crate::config::{Config, STORAGE}; 13 | 14 | /// List toolboxes 15 | #[derive(Args, PartialEq, Eq, Debug)] 16 | pub struct List {} 17 | 18 | struct Row { 19 | name: String, 20 | config: Config, 21 | } 22 | 23 | impl Row { 24 | fn new(name: String) -> eyre::Result { 25 | let config = Config::read_or_new(&name)?; 26 | Ok(Self { name, config }) 27 | } 28 | } 29 | 30 | impl Tabled for Row { 31 | const LENGTH: usize = 4; 32 | 33 | fn fields(&self) -> Vec> { 34 | vec![ 35 | Cow::Borrowed(&self.name), 36 | Cow::Borrowed(&self.config.shell), 37 | Cow::Borrowed(&self.config.hostname), 38 | Cow::Borrowed(&self.config.image), 39 | ] 40 | } 41 | fn headers() -> Vec> { 42 | ["name", "shell", "hostname", "image"] 43 | .into_iter() 44 | .map(Cow::from) 45 | .collect() 46 | } 47 | } 48 | 49 | pub fn list() -> eyre::Result<()> { 50 | let home = env::var("HOME").wrap_err("Could not find current home")?; 51 | let storage = format!("{home}/{STORAGE}/images"); 52 | let paths = match std::fs::read_dir(storage) { 53 | Ok(paths) => paths, 54 | Err(_) => { 55 | help(); 56 | return Ok(()); 57 | } 58 | }; 59 | let rows: Vec = paths 60 | .filter_map(|p| p.ok()?.file_name().into_string().ok()) 61 | .filter_map(|p| Row::new(p).ok()) 62 | .collect(); 63 | if rows.is_empty() { 64 | help(); 65 | } else { 66 | let mut table = Table::new(rows); 67 | let table = table.with(Style::modern()); 68 | print!("{table}"); 69 | } 70 | Ok(()) 71 | } 72 | 73 | fn help() { 74 | println!("No images could be found, maybe you want to create a new one first:"); 75 | println!(); 76 | println!("\t unbox create -i -e "); 77 | println!("\t unbox create -t "); 78 | } 79 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #![forbid(unsafe_code)] 6 | 7 | use clap::{Parser, Subcommand}; 8 | use color_eyre::eyre; 9 | 10 | use unbox::*; 11 | 12 | /// Unshare a toolbox 13 | #[derive(Parser, PartialEq, Eq, Debug)] 14 | #[clap(version, about)] 15 | struct UnBox { 16 | #[clap(subcommand)] 17 | subcommands: Subcommands, 18 | } 19 | 20 | #[derive(Subcommand, PartialEq, Eq, Debug)] 21 | enum Subcommands { 22 | Create(create::Create), 23 | #[clap(alias = "cfg")] 24 | Configure(config::Configure), 25 | Enter(run::Enter), 26 | Run(run::Run), 27 | #[clap(alias = "rm")] 28 | Remove(remove::Remove), 29 | #[clap(alias = "ls")] 30 | List(list::List), 31 | #[clap(hide = true)] 32 | SetMappings(namespaces::SetMappings), 33 | } 34 | 35 | fn main() -> eyre::Result<()> { 36 | // color_eyre::install()?; 37 | color_eyre::config::HookBuilder::default() 38 | .display_env_section(false) 39 | .install()?; 40 | config::setup()?; 41 | let cmd = UnBox::parse(); 42 | 43 | match cmd.subcommands { 44 | Subcommands::Create(args) => create::create(args), 45 | Subcommands::Enter(args) => run::nsexec(run::Execute::Enter(args)), 46 | Subcommands::Run(args) => run::nsexec(run::Execute::Run(args)), 47 | Subcommands::Configure(args) => config::configure(args), 48 | Subcommands::Remove(args) => remove::remove(args), 49 | Subcommands::List(_) => list::list(), 50 | Subcommands::SetMappings(_) => namespaces::set_mappings(), 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/namespaces.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::fmt::Display; 6 | use std::fs::{read_link, symlink_metadata}; 7 | use std::io::Write; 8 | use std::os::unix::prelude::CommandExt; 9 | use std::path::PathBuf; 10 | use std::process::{Child, Command, Stdio}; 11 | 12 | use clap::Args; 13 | use color_eyre::eyre; 14 | use color_eyre::eyre::WrapErr; 15 | use nix::sched::{unshare, CloneFlags}; 16 | use nix::unistd::{pivot_root, sethostname}; 17 | use std::ffi::{OsStr, OsString}; 18 | 19 | use crate::config::MountInfo; 20 | 21 | // Setup the uid and gid mappings inside the namespace 22 | /// Internal subcommand. Should not be used directly 23 | #[derive(Args, PartialEq, Eq, Debug)] 24 | pub struct SetMappings {} 25 | 26 | pub fn set_mappings() -> eyre::Result<()> { 27 | let mut uid_map = String::new(); 28 | let mut gid_map = String::new(); 29 | std::io::stdin() 30 | .read_line(&mut uid_map) 31 | .expect("Parent gives us uid_map through stdin"); 32 | std::io::stdin() 33 | .read_line(&mut gid_map) 34 | .expect("Parent gives us gid_map through stdin"); 35 | 36 | std::thread::scope(|s| { 37 | s.spawn(|| { 38 | let mut uid_map = match spawn("newuidmap", uid_map.trim().split(' ')) 39 | .wrap_err("Failure to write uid_map") 40 | { 41 | Ok(child) => child, 42 | Err(e) => { 43 | eprintln!("{e}"); 44 | std::process::exit(1); 45 | } 46 | }; 47 | uid_map.wait().expect("Failure to wait for uid_map"); 48 | }); 49 | s.spawn(|| { 50 | let mut gid_map = match spawn("newgidmap", gid_map.trim().split(' ')) 51 | .wrap_err("Failure to write gid_map") 52 | { 53 | Ok(child) => child, 54 | Err(e) => { 55 | eprintln!("{e}"); 56 | std::process::exit(1); 57 | } 58 | }; 59 | gid_map.wait().expect("Failure to wait for gid_map"); 60 | }); 61 | }); 62 | 63 | Ok(()) 64 | } 65 | 66 | fn spawn<'a, C, A>(cmd: C, args: A) -> eyre::Result 67 | where 68 | C: AsRef, 69 | A: Iterator, 70 | { 71 | Command::new(cmd) 72 | .args(args) 73 | .spawn() 74 | .wrap_err("Could not spawn the requested command") 75 | } 76 | 77 | pub struct Namespace { 78 | mapper: Child, 79 | typestate: std::marker::PhantomData, 80 | } 81 | 82 | #[allow(dead_code)] 83 | pub struct Setup; 84 | 85 | pub struct Pivoter; 86 | pub struct Toolbox; 87 | 88 | impl Namespace { 89 | pub fn wait(&mut self) { 90 | self.mapper.wait().expect("interrupted"); 91 | } 92 | } 93 | 94 | impl Namespace { 95 | pub fn start( 96 | flags: CloneFlags, 97 | uid_mappings: &[Mapping<'_>], 98 | gid_mappings: &[Mapping<'_>], 99 | ) -> eyre::Result> { 100 | let pid = std::process::id().to_string(); 101 | let child = Command::new("/proc/self/exe") 102 | .arg("set-mappings") 103 | .stdin(Stdio::piped()) 104 | .spawn() 105 | .wrap_err("Could not spawn child to set up mappings")?; 106 | 107 | unshare(flags).wrap_err("Could not change namespace")?; 108 | 109 | let child_in = &mut child.stdin.as_ref().unwrap(); 110 | let uid_argv = mappings_argv(&pid, uid_mappings); 111 | let gid_argv = mappings_argv(&pid, gid_mappings); 112 | writeln!(child_in, "{}", uid_argv).expect("communication failed"); 113 | writeln!(child_in, "{}", gid_argv).expect("communication failed"); 114 | 115 | let next = Namespace { 116 | mapper: child, 117 | typestate: std::marker::PhantomData, 118 | }; 119 | Ok(next) 120 | } 121 | } 122 | 123 | fn mappings_argv<'a>(pid: &'a str, mappings: &[Mapping<'a>]) -> String { 124 | let mut argv = String::with_capacity(10 * mappings.len()); 125 | argv.push_str(pid); 126 | argv.push(' '); 127 | for map in mappings { 128 | argv.push_str(map.inside); 129 | argv.push(' '); 130 | argv.push_str(map.outside); 131 | argv.push(' '); 132 | argv.push_str(map.len); 133 | argv.push(' '); 134 | } 135 | argv 136 | } 137 | 138 | impl Namespace { 139 | pub fn pivot(self, new_root: &OsStr, old_root: &OsStr) -> eyre::Result> { 140 | // We have to bind mount the new root to itself because it is part of the old root 141 | bind_mount(new_root, new_root)?; 142 | pivot_root(new_root, old_root).wrap_err("Could not pivot into the new root")?; 143 | let next = Namespace { 144 | mapper: self.mapper, 145 | typestate: std::marker::PhantomData, 146 | }; 147 | Ok(next) 148 | } 149 | } 150 | 151 | impl Namespace { 152 | pub fn mounts(&self, mounts: I) -> eyre::Result<()> 153 | where 154 | I: Iterator, 155 | { 156 | mounts 157 | .map(|m| (follow_symlink(m.source), m.target)) 158 | .try_for_each(|m| bind_mount(&m.0, &m.1)) 159 | } 160 | 161 | pub fn hostname(&self, name: &str) -> eyre::Result<()> { 162 | sethostname(name).wrap_err("Could not change the hostname") 163 | } 164 | 165 | pub fn spawn(&mut self, cmd: S, args: &[S]) -> eyre::Result<()> 166 | where 167 | S: AsRef, 168 | { 169 | self.wait(); 170 | Command::new(cmd).args(args).exec(); 171 | eyre::bail!("Could not execute the requested command") 172 | } 173 | } 174 | 175 | pub struct Mapping<'a> { 176 | pub inside: &'a str, 177 | pub outside: &'a str, 178 | pub len: &'a str, 179 | } 180 | 181 | impl Display for Mapping<'_> { 182 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 183 | writeln!(f, "{} {} {}", self.inside, self.outside, self.len) 184 | } 185 | } 186 | 187 | fn bind_mount(source: &OsStr, target: &OsStr) -> eyre::Result<()> { 188 | use nix::mount::MsFlags; 189 | nix::mount::mount::( 190 | Some(source), 191 | target, 192 | None, 193 | MsFlags::MS_BIND | MsFlags::MS_REC, 194 | None, 195 | ) 196 | .wrap_err(format!( 197 | "Could not bind mount the directory {source:?} to {target:?}" 198 | )) 199 | } 200 | 201 | fn follow_symlink(path: OsString) -> OsString { 202 | match symlink_metadata(&path) { 203 | Ok(meta) if meta.is_symlink() => { 204 | let path = PathBuf::from(path); 205 | let link = read_link(&path).expect("is a valid symlink"); 206 | if link.is_absolute() { 207 | link.into() 208 | } else { 209 | let mut real = OsString::with_capacity(path.capacity() + link.capacity()); 210 | let parent = path.parent().expect("path has parent"); 211 | real.push(parent); 212 | real.push("/"); 213 | real.push(link); 214 | real 215 | } 216 | } 217 | _ => path, 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/remove.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use std::env; 6 | use std::fs::Permissions; 7 | use std::os::unix::prelude::PermissionsExt; 8 | 9 | use clap::Args; 10 | use color_eyre::eyre; 11 | use color_eyre::eyre::WrapErr; 12 | use walkdir::WalkDir; 13 | 14 | use crate::config::{Config, STORAGE}; 15 | 16 | /// Remove a toolbox 17 | #[derive(Args, PartialEq, Eq, Debug)] 18 | pub struct Remove { 19 | #[clap(value_parser)] 20 | /// Names of the toolboxes to be removed 21 | pub names: Vec, 22 | } 23 | 24 | pub fn remove(args: Remove) -> eyre::Result<()> { 25 | for name in args.names { 26 | remove_one(name)?; 27 | } 28 | Ok(()) 29 | } 30 | 31 | pub fn remove_one(name: String) -> eyre::Result<()> { 32 | let home = env::var("HOME").wrap_err("Could not find current home")?; 33 | let meta = format!("{home}/{}/meta/{}.toml", STORAGE, name); 34 | let config = 35 | Config::read_or_new(&name).wrap_err("Could not get configuration for the toolbox")?; 36 | for entry in WalkDir::new(&config.image) 37 | .into_iter() 38 | .filter_map(|e| e.ok()) 39 | { 40 | let perms = Permissions::from_mode(0o777); 41 | // We change the permissions on directories to avoid errors on read-only directories 42 | if entry.file_type().is_dir() { 43 | std::fs::set_permissions(entry.path(), perms).expect("we own the files"); 44 | } 45 | } 46 | // The error is ignored because if the file does not exist we do not need to remove it. 47 | let _ = std::fs::remove_file(meta); 48 | std::fs::remove_dir_all(config.image).wrap_err("Could not remove the selected toolbox") 49 | } 50 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use clap::Args; 6 | use color_eyre::eyre; 7 | use color_eyre::eyre::WrapErr; 8 | use nix::sched::CloneFlags; 9 | use std::env; 10 | use std::ffi::OsString; 11 | 12 | use crate::config::Config; 13 | use crate::namespaces::{Mapping, Namespace}; 14 | 15 | pub enum Execute { 16 | Run(Run), 17 | Enter(Enter), 18 | } 19 | 20 | /// Enter a toolbox 21 | #[derive(Args, PartialEq, Eq, Debug)] 22 | pub struct Enter { 23 | #[clap(value_parser)] 24 | /// Name of the toolbox 25 | name: String, 26 | } 27 | 28 | /// Run a command in a toolbox 29 | #[derive(Args, PartialEq, Eq, Debug)] 30 | pub struct Run { 31 | #[clap(value_parser)] 32 | /// Name of the toolbox 33 | pub name: String, 34 | #[clap(value_parser)] 35 | /// Command to run 36 | pub cmd: String, 37 | /// Command arguments 38 | #[clap(value_parser)] 39 | pub args: Vec, 40 | } 41 | 42 | pub fn nsexec(args: Execute) -> eyre::Result<()> { 43 | let flags = CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWUTS | CloneFlags::CLONE_NEWNS; 44 | 45 | let uid = users::get_current_uid().to_string(); 46 | let gid = users::get_current_gid().to_string(); 47 | let pivot = Namespace::start(flags, &id_map(&uid), &id_map(&gid))?; 48 | 49 | let config = configuration(&args)?; 50 | let new_root = &config.image; 51 | let old_root = format!("{new_root}/host"); 52 | let mounts = config.mounts().filter_map(|m| m.ok()); 53 | 54 | env::set_var("PATH", extend_path()); 55 | env::set_var("HOME", &config.home); 56 | 57 | let mut toolbox = pivot.pivot(new_root.as_ref(), old_root.as_ref())?; 58 | toolbox.mounts(mounts)?; 59 | toolbox.hostname(&config.hostname)?; 60 | match args { 61 | Execute::Enter(_) => toolbox.spawn(config.shell, &[]), 62 | Execute::Run(args) => toolbox.spawn(args.cmd, &args.args), 63 | } 64 | } 65 | 66 | fn id_map(guid: &str) -> [Mapping<'_>; 2] { 67 | [ 68 | Mapping { 69 | inside: "0", 70 | outside: guid, 71 | len: "1", 72 | }, 73 | Mapping { 74 | inside: "1", 75 | outside: "100000", 76 | len: "65536", 77 | }, 78 | ] 79 | } 80 | 81 | fn configuration(args: &Execute) -> eyre::Result { 82 | let name = match args { 83 | Execute::Enter(args) => &args.name, 84 | Execute::Run(args) => &args.name, 85 | }; 86 | Config::read_or_new(name).wrap_err("Could not get configuration for the toolbox") 87 | } 88 | 89 | fn extend_path() -> OsString { 90 | let mut path = env::var_os("PATH").expect("PATH needs to exist"); 91 | path.push(":/bin"); 92 | path.push(":/sbin"); 93 | path 94 | } 95 | -------------------------------------------------------------------------------- /tests/create_alpine.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use unbox::create::*; 6 | use unbox::remove::*; 7 | 8 | #[test] 9 | fn create_alpine() { 10 | let args = Create { 11 | name: "alpine-test".into(), 12 | tar: None, 13 | image: Some("docker.io/alpine:edge".into()), 14 | engine: Some(Engine::Podman), 15 | shell: None, 16 | quiet: true, 17 | }; 18 | create(args).unwrap(); 19 | 20 | let args = Remove { 21 | names: vec!["alpine-test".into()], 22 | }; 23 | remove(args).unwrap() 24 | } 25 | -------------------------------------------------------------------------------- /tests/create_arch.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use unbox::create::*; 6 | use unbox::remove::*; 7 | 8 | #[test] 9 | fn create_arch() { 10 | let args = Create { 11 | name: "arch-test".into(), 12 | tar: None, 13 | image: Some("docker.io/archlinux".into()), 14 | engine: Some(Engine::Podman), 15 | shell: None, 16 | quiet: true, 17 | }; 18 | create(args).unwrap(); 19 | 20 | let args = Remove { 21 | names: vec!["arch-test".into()], 22 | }; 23 | remove(args).unwrap() 24 | } 25 | -------------------------------------------------------------------------------- /tests/create_ubuntu.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use unbox::create::*; 6 | use unbox::remove::*; 7 | 8 | #[test] 9 | fn create_ubuntu() { 10 | let args = Create { 11 | name: "ubuntu-test".into(), 12 | tar: None, 13 | image: Some("docker.io/ubuntu".into()), 14 | engine: Some(Engine::Podman), 15 | shell: None, 16 | quiet: true, 17 | }; 18 | create(args).unwrap(); 19 | 20 | let args = Remove { 21 | names: vec!["ubuntu-test".into()], 22 | }; 23 | remove(args).unwrap() 24 | } 25 | -------------------------------------------------------------------------------- /tests/run_true.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use unbox::run::*; 6 | 7 | #[test] 8 | fn run_true() { 9 | let args = Execute::Run(Run { 10 | name: "exec-tests".into(), 11 | cmd: "true".into(), 12 | args: vec![], 13 | }); 14 | nsexec(args).unwrap(); 15 | } 16 | 17 | #[test] 18 | fn run_bin_true() { 19 | let args = Execute::Run(Run { 20 | name: "exec-tests".into(), 21 | cmd: "/bin/true".into(), 22 | args: vec![], 23 | }); 24 | nsexec(args).unwrap(); 25 | } 26 | --------------------------------------------------------------------------------