diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e95354e..df3b9aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Build +name: Rust on: [push, pull_request] @@ -16,3 +16,5 @@ jobs: - uses: actions/checkout@v2 - name: Build run: cargo build --verbose + - name: Test + run: cargo test diff --git a/.gitignore b/.gitignore index 55dfc21..0198098 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ /TREE /SRCS /STATES -/.cargo .#* *.tar* diff --git a/Cargo.lock b/Cargo.lock index 9ebea65..51321b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,15 +23,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "anstream" version = "0.6.18" @@ -332,9 +323,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.5" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "jobserver", "libc", @@ -354,65 +345,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "ciel" -version = "3.8.8" +name = "ciel-rs" +version = "3.9.0" dependencies = [ "adler32", + "anyhow", "ar", "bincode", + "clap", + "clap_complete", + "console", + "ctrlc", + "dialoguer", + "dotenvy", "faster-hex", "flate2", "fs3", "git2", + "indicatif", "inotify", + "libc", "libmount", "libsystemd-sys", - "log", "nix 0.29.0", "rand", "rayon", - "serde", - "sha2", - "tar", - "tempfile", - "test-log", - "thiserror 2.0.8", - "time", - "toml", - "walkdir", - "xattr", - "xz2", - "zbus", - "zstd", -] - -[[package]] -name = "ciel-cli" -version = "3.8.8" -dependencies = [ - "anyhow", - "ciel", - "clap", - "clap_complete", - "console", - "dialoguer", - "dotenvy", - "fs3", - "git2", - "indicatif", - "log", - "nix 0.29.0", "reqwest", "serde", "sha2", "tabwriter", "tar", "tempfile", + "time", + "toml", "unsquashfs-wrapper", "walkdir", "which", + "xattr", "xz2", "zbus", + "zstd", ] [[package]] @@ -434,13 +406,14 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] name = "clap_complete" -version = "4.5.40" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2e663e3e3bed2d32d065a8404024dad306e699a04263ec59919529f803aee9" +checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" dependencies = [ "clap", ] @@ -468,15 +441,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.10" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "once_cell", - "unicode-width 0.2.0", - "windows-sys 0.59.0", + "unicode-width 0.1.14", + "windows-sys 0.52.0", ] [[package]] @@ -515,9 +488,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -534,9 +507,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" @@ -548,6 +521,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix 0.29.0", + "windows-sys 0.59.0", +] + [[package]] name = "deranged" version = "0.3.11" @@ -607,9 +590,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" -version = "1.0.0" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" @@ -647,33 +630,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", -] - -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - -[[package]] -name = "env_logger" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "log", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -960,6 +916,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "1.2.0" @@ -1002,9 +967,9 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "hyper" -version = "1.5.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -1022,9 +987,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http", @@ -1301,9 +1266,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libgit2-sys" @@ -1406,15 +1371,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - [[package]] name = "memchr" version = "2.7.4" @@ -1438,9 +1394,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", ] @@ -1497,16 +1453,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -1588,12 +1534,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking" version = "2.2.1" @@ -1773,50 +1713,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - [[package]] name = "reqwest" version = "0.12.9" @@ -1983,9 +1879,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.13.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -2081,15 +1977,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shell-words" version = "1.1.0" @@ -2252,25 +2139,13 @@ dependencies = [ ] [[package]] -name = "test-log" -version = "0.2.16" +name = "terminal_size" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dffced63c2b5c7be278154d76b479f9f9920ed34e7574201407f0b14e2bbb93" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ - "env_logger", - "test-log-macros", - "tracing-subscriber", -] - -[[package]] -name = "test-log-macros" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5999e24eaa32083191ba4e425deb75cdf25efefabe5aaccb7446dd0d4122a3f5" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "rustix", + "windows-sys 0.59.0", ] [[package]] @@ -2284,11 +2159,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.8" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" dependencies = [ - "thiserror-impl 2.0.8", + "thiserror-impl 2.0.7", ] [[package]] @@ -2304,9 +2179,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.8" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" dependencies = [ "proc-macro2", "quote", @@ -2481,35 +2356,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] [[package]] @@ -2560,7 +2406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c233ab927e810aac155e6a22ecc44a6aaf8d66682bf7c287c80c68e2680110c9" dependencies = [ "pty-process", - "thiserror 2.0.8", + "thiserror 2.0.7", "which", ] @@ -2599,12 +2445,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "vcpkg" version = "0.2.15" @@ -2731,12 +2571,12 @@ dependencies = [ [[package]] name = "which" -version = "7.0.1" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028" +checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" dependencies = [ "either", - "env_home", + "home", "rustix", "winsafe", ] @@ -2967,9 +2807,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.2.0" +version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1" +checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" dependencies = [ "async-broadcast", "async-executor", @@ -3003,9 +2843,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.2.0" +version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d49ebc960ceb660f2abe40a5904da975de6986f2af0d7884b39eec6528c57" +checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 5eb021a..242db6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,65 +1,56 @@ -[workspace] -resolver = "2" -members = [ - ".", - "cli/", -] - -[workspace.package] -version = "3.8.8" +[package] +name = "ciel-rs" +version = "3.9.0" description = "An nspawn container manager" license = "MIT" -authors = ["liushuyu ", "xtex "] +authors = ["liushuyu "] repository = "https://github.com/AOSC-Dev/ciel-rs" +resolver = "2" edition = "2021" -[package] -name = "ciel" -version.workspace = true -description.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -edition.workspace = true - [dependencies] -zbus = { version = "^5", features = ["blocking"] } -nix = { version = "0.29", features = ["fs", "hostname", "mount", "signal", "user"] } +console = "0.15" +zbus = "^5" +dialoguer = { version = "0.11", features = ["fuzzy-select"] } +indicatif = "0.17" +nix = { version = "0.29", features = ["fs", "hostname", "mount", "user"] } toml = "0.8" bincode = "1.3" serde = { version = "1.0", features = ["derive"] } +reqwest = { version = "0.12", features = ["blocking", "json"] } git2 = "0.19" +tar = "0.4" +xz2 = "0.1" libmount = { git = "https://github.com/liushuyu/libmount", rev = "6fe8dba03a6404dfe1013995dd17af1c4e21c97b" } +libc = "0.2" adler32 = "1.2" rayon = "1.10" -tempfile = "3.14" +tempfile = "3.10" +anyhow = "1.0" libsystemd-sys = "0.9" walkdir = "2" xattr = "^1" -rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } +rand = "0.8" +dotenvy = "0.15" +which = "7.0" +sha2 = "0.10" time = { version = "0.3", default-features = false, features = ["serde-human-readable", "macros"] } fs3 = "0.5" +clap = { version = "^4", features = ["wrap_help", "string", "env"] } +ctrlc = "3.4.4" +# repo scan ar = "0.9" faster-hex = "0.10" flate2 = "1.0" +tabwriter = { version = "^1", features = ["ansi_formatting"] } +unsquashfs-wrapper = "0.3" inotify = "0.11" zstd = "0.13.2" -thiserror = "2.0.8" -log = "0.4.22" -test-log = { version = "0.2.16", features = ["log"] } -sha2 = "0.10.8" -tar = "0.4.43" -xz2 = "0.1.7" + +[build-dependencies] +clap = { version = "^4", features = ["string", "env"] } +clap_complete = "^4" +anyhow = "^1" [profile.release] lto = true - -[workspace.metadata.release] -shared-version = true -consolidate-commits = true -pre-release-commit-message = "v{{version}}" -tag-message = "v{{version}}" -tag-name = "v{{version}}" -# we cannot publish to crates.io due to the libmount git dep -publish = false -verify = true diff --git a/README.md b/README.md index 19cbb61..96625db 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,15 @@ ciel --help ## Installation ```bash -cargo build --release --workspace +cargo build --release +install -Dm755 target/release/ciel-rs /usr/local/bin/ciel PREFIX=/usr/local ./install-assets.sh ``` ## Dependencies Building: -- Rust w/ Cargo (Rust 1.83.0+) +- Rust w/ Cargo (Rust 1.80.0+) - C compiler - pkg-config (for detecting C library dependencies) - make (when GCC LTO is used, not needed for Clang) @@ -33,4 +34,3 @@ Runtime: Runtime Kernel: - Overlay file system -- tmpfs diff --git a/cli/build.rs b/build.rs similarity index 100% rename from cli/build.rs rename to build.rs diff --git a/cli/Cargo.toml b/cli/Cargo.toml deleted file mode 100644 index 27bb8ad..0000000 --- a/cli/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "ciel-cli" -version.workspace = true -description.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -edition.workspace = true - -[dependencies] -anyhow = "1.0.94" -ciel = { version = "3.8.8", path = ".." } -clap = { version = "^4", features = ["string", "env"] } -console = "0.15.10" -dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } -dotenvy = "0.15.7" -fs3 = "0.5.0" -git2 = "0.19.0" -indicatif = "0.17.9" -log = { version = "0.4.22", features = ["max_level_debug", "release_max_level_info", "std"] } -nix = "0.29.0" -reqwest = { version = "0.12.9", features = ["blocking", "json"] } -serde = { version = "1.0.216", features = ["derive"] } -sha2 = "0.10.8" -tabwriter = { version = "1.4.0", features = ["ansi_formatting"] } -tar = "0.4.43" -tempfile = "3.14.0" -unsquashfs-wrapper = "0.3.0" -walkdir = "2.5.0" -which = "7.0.1" -xz2 = "0.1.7" -zbus = { version = "5.2.0", features = ["blocking"] } - -[build-dependencies] -clap = { version = "^4", features = ["string", "env"] } -clap_complete = "^4" -anyhow = "^1" - -[[bin]] -name = "ciel" -path = "src/main.rs" diff --git a/cli/completions/_ciel b/cli/completions/_ciel deleted file mode 100644 index 2c0a315..0000000 --- a/cli/completions/_ciel +++ /dev/null @@ -1,824 +0,0 @@ -#compdef ciel - -autoload -U is-at-least - -_ciel() { - typeset -A opt_args - typeset -a _arguments_options - local ret=1 - - if is-at-least 5.2; then - _arguments_options=(-s -S -C) - else - _arguments_options=(-s -C) - fi - - local context curcontext="$curcontext" state line - _arguments "${_arguments_options[@]}" : \ -'-C+[Set the CIEL! working directory]:DIR:_default' \ -'-q[shhhhhh!]' \ -'--quiet[shhhhhh!]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'-V[Print version]' \ -'--version[Print version]' \ -":: :_ciel_commands" \ -"*::: :->ciel" \ -&& ret=0 - case $state in - (ciel) - words=($line[1] "${words[@]}") - (( CURRENT += 1 )) - curcontext="${curcontext%:*:*}:ciel-command-$line[1]:" - case $line[1] in - (version) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(list) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(new) -_arguments "${_arguments_options[@]}" : \ -'--rootfs=[Specify the tarball or squashfs to load after initialization]: :_default' \ -'--sha256=[Specify the SHA-256 checksum of OS tarball]: :_default' \ -'-a+[Specify the architecture of the workspace]: :_default' \ -'--arch=[Specify the architecture of the workspace]: :_default' \ -'--tree=[URL to the abbs tree git repository]: :_default' \ -'-m+[Maintainer information]: :_default' \ -'--maintainer=[Maintainer information]: :_default' \ -'--dnssec=[Enable DNSSEC]: :(true false)' \ -'--local-repo=[Enable local package repository]: :(true false)' \ -'--source-cache=[Enable local source caches]: :(true false)' \ -'--branch-exclusive-output=[Use different OUTPUT directory for branches]: :(true false)' \ -'--volatile-mount=[Enable volatile mount]: :(true false)' \ -'--use-apt=[Force to use APT]: :(true false)' \ -'--add-repo=[Add an extra APT repository]:repo:_default' \ -'--remove-repo=[Remove an extra APT repository]:repo:_default' \ -'--add-nspawn-opt=[Add an extra nspawn option]:nspawn-opt:_default' \ -'--remove-nspawn-opt=[Remove an extra nspawn option]:nspawn-opt:_default' \ -'(--rootfs --sha256)--no-load-os[Don'\''t load OS automatically after initialization]' \ -'(--tree)--no-load-tree[Don'\''t load abbs tree automatically after initialization]' \ -'--unset-repo[Remove all extra APT repository]' \ -'--unset-nspawn-opt[Remove all extra nspawn option]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(farewell) -_arguments "${_arguments_options[@]}" : \ -'-f[Force perform deletion without user confirmation]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(load-os) -_arguments "${_arguments_options[@]}" : \ -'--sha256=[Specify the SHA-256 checksum of OS tarball]: :_default' \ -'-a+[Specify the target architecture for fetching OS tarball]: :_default' \ -'--arch=[Specify the target architecture for fetching OS tarball]: :_default' \ -'-f[Force override the loaded system]' \ -'--force[Force override the loaded system]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'::URL -- URL or path to the tarball or squashfs:_default' \ -&& ret=0 -;; -(update-os) -_arguments "${_arguments_options[@]}" : \ -'--local-repo=[Enable local package repository]: :(true false)' \ -'--tmpfs=[Enable tmpfs]: :(true false)' \ -'--tmpfs-size=[Size of tmpfs to use, in MiB]: :_default' \ -'--ro-tree=[Mount TREE as read-only]: :(true false)' \ -'--output=[Path to output directory]: :_files' \ -'--add-repo=[Add an extra APT repository]:repo:_default' \ -'--remove-repo=[Remove an extra APT repository]:repo:_default' \ -'--add-nspawn-opt=[Add an extra nspawn option]:nspawn-opt:_default' \ -'--remove-nspawn-opt=[Remove an extra nspawn option]:nspawn-opt:_default' \ -'--force-use-apt[Use apt to update-os]' \ -'(--tmpfs-size)--unset-tmpfs-size[Reset tmpfs size to default]' \ -'(--output)--unset-output[Use default output directory]' \ -'--unset-repo[Remove all extra APT repository]' \ -'--unset-nspawn-opt[Remove all extra nspawn option]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(instconf) -_arguments "${_arguments_options[@]}" : \ -'-i+[Instance to be configured]: :_default' \ -'--local-repo=[Enable local package repository]: :(true false)' \ -'--tmpfs=[Enable tmpfs]: :(true false)' \ -'--tmpfs-size=[Size of tmpfs to use, in MiB]: :_default' \ -'--ro-tree=[Mount TREE as read-only]: :(true false)' \ -'--output=[Path to output directory]: :_files' \ -'--add-repo=[Add an extra APT repository]:repo:_default' \ -'--remove-repo=[Remove an extra APT repository]:repo:_default' \ -'--add-nspawn-opt=[Add an extra nspawn option]:nspawn-opt:_default' \ -'--remove-nspawn-opt=[Remove an extra nspawn option]:nspawn-opt:_default' \ -'--force-no-rollback[Do not rollback instances to apply configuration]' \ -'(--tmpfs-size)--unset-tmpfs-size[Reset tmpfs size to default]' \ -'(--output)--unset-output[Use default output directory]' \ -'--unset-repo[Remove all extra APT repository]' \ -'--unset-nspawn-opt[Remove all extra nspawn option]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(config) -_arguments "${_arguments_options[@]}" : \ -'-m+[Maintainer information]: :_default' \ -'--maintainer=[Maintainer information]: :_default' \ -'--dnssec=[Enable DNSSEC]: :(true false)' \ -'--local-repo=[Enable local package repository]: :(true false)' \ -'--source-cache=[Enable local source caches]: :(true false)' \ -'--branch-exclusive-output=[Use different OUTPUT directory for branches]: :(true false)' \ -'--volatile-mount=[Enable volatile mount]: :(true false)' \ -'--use-apt=[Force to use APT]: :(true false)' \ -'--add-repo=[Add an extra APT repository]:repo:_default' \ -'--remove-repo=[Remove an extra APT repository]:repo:_default' \ -'--add-nspawn-opt=[Add an extra nspawn option]:nspawn-opt:_default' \ -'--remove-nspawn-opt=[Remove an extra nspawn option]:nspawn-opt:_default' \ -'--force-no-rollback[Do not rollback instances to apply configuration]' \ -'--unset-repo[Remove all extra APT repository]' \ -'--unset-nspawn-opt[Remove all extra nspawn option]' \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(load-tree) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -'::URL -- URL to the git repository:_default' \ -&& ret=0 -;; -(add) -_arguments "${_arguments_options[@]}" : \ -'--local-repo=[Enable local package repository]: :(true false)' \ -'--tmpfs=[Enable tmpfs]: :(true false)' \ -'--tmpfs-size=[Size of tmpfs to use, in MiB]: :_default' \ -'--ro-tree=[Mount TREE as read-only]: :(true false)' \ -'--output=[Path to output directory]: :_files' \ -'--add-repo=[Add an extra APT repository]:repo:_default' \ -'--remove-repo=[Remove an extra APT repository]:repo:_default' \ -'--add-nspawn-opt=[Add an extra nspawn option]:nspawn-opt:_default' \ -'--remove-nspawn-opt=[Remove an extra nspawn option]:nspawn-opt:_default' \ -'(--tmpfs-size)--unset-tmpfs-size[Reset tmpfs size to default]' \ -'(--output)--unset-output[Use default output directory]' \ -'--unset-repo[Remove all extra APT repository]' \ -'--unset-nspawn-opt[Remove all extra nspawn option]' \ -'-h[Print help]' \ -'--help[Print help]' \ -':INSTANCE:_default' \ -&& ret=0 -;; -(del) -_arguments "${_arguments_options[@]}" : \ -'-a[]' \ -'--all[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::INSTANCE:_default' \ -&& ret=0 -;; -(mount) -_arguments "${_arguments_options[@]}" : \ -'-a[]' \ -'--all[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::INSTANCE:_default' \ -&& ret=0 -;; -(boot) -_arguments "${_arguments_options[@]}" : \ -'-a[]' \ -'--all[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::INSTANCE:_default' \ -&& ret=0 -;; -(stop) -_arguments "${_arguments_options[@]}" : \ -'-a[]' \ -'--all[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::INSTANCE:_default' \ -&& ret=0 -;; -(down) -_arguments "${_arguments_options[@]}" : \ -'-a[]' \ -'--all[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::INSTANCE:_default' \ -&& ret=0 -;; -(rollback) -_arguments "${_arguments_options[@]}" : \ -'-a[]' \ -'--all[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::INSTANCE:_default' \ -&& ret=0 -;; -(commit) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -':INSTANCE:_default' \ -&& ret=0 -;; -(shell) -_arguments "${_arguments_options[@]}" : \ -'-i+[Instance to be used]: :_default' \ -'(-i)--local-repo=[Enable local package repository]: :(true false)' \ -'(-i)--tmpfs=[Enable tmpfs]: :(true false)' \ -'(-i)--tmpfs-size=[Size of tmpfs to use, in MiB]: :_default' \ -'(-i)--ro-tree=[Mount TREE as read-only]: :(true false)' \ -'(-i)--output=[Path to output directory]: :_files' \ -'(-i)--add-repo=[Add an extra APT repository]:repo:_default' \ -'(-i)--remove-repo=[Remove an extra APT repository]:repo:_default' \ -'(-i)--add-nspawn-opt=[Add an extra nspawn option]:nspawn-opt:_default' \ -'(-i)--remove-nspawn-opt=[Remove an extra nspawn option]:nspawn-opt:_default' \ -'(--tmpfs-size -i)--unset-tmpfs-size[Reset tmpfs size to default]' \ -'(--output -i)--unset-output[Use default output directory]' \ -'(-i)--unset-repo[Remove all extra APT repository]' \ -'(-i)--unset-nspawn-opt[Remove all extra nspawn option]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::COMMANDS:_default' \ -&& ret=0 -;; -(run) -_arguments "${_arguments_options[@]}" : \ -'-i+[Instance to run command in]: :_default' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::COMMANDS:_default' \ -&& ret=0 -;; -(build) -_arguments "${_arguments_options[@]}" : \ -'-i+[Instance to be used]: :_default' \ -'(-i)--local-repo=[Enable local package repository]: :(true false)' \ -'(-i)--tmpfs=[Enable tmpfs]: :(true false)' \ -'(-i)--tmpfs-size=[Size of tmpfs to use, in MiB]: :_default' \ -'(-i)--ro-tree=[Mount TREE as read-only]: :(true false)' \ -'(-i)--output=[Path to output directory]: :_files' \ -'(-i)--add-repo=[Add an extra APT repository]:repo:_default' \ -'(-i)--remove-repo=[Remove an extra APT repository]:repo:_default' \ -'(-i)--add-nspawn-opt=[Add an extra nspawn option]:nspawn-opt:_default' \ -'(-i)--remove-nspawn-opt=[Remove an extra nspawn option]:nspawn-opt:_default' \ -'(-g --stage-select)-c+[Resume from a Ciel checkpoint]: :_default' \ -'(-g --stage-select)--resume=[Resume from a Ciel checkpoint]: :_default' \ -'(--tmpfs-size -i)--unset-tmpfs-size[Reset tmpfs size to default]' \ -'(--output -i)--unset-output[Use default output directory]' \ -'(-i)--unset-repo[Remove all extra APT repository]' \ -'(-i)--unset-nspawn-opt[Remove all extra nspawn option]' \ -'-g[Fetch package sources only]' \ -'--stage-select[Select the starting point for a build]' \ -'(-i)--always-discard[Destory ephemeral containers if the build fails]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::PACKAGES:_default' \ -&& ret=0 -;; -(repo) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -":: :_ciel__repo_commands" \ -"*::: :->repo" \ -&& ret=0 - - case $state in - (repo) - words=($line[1] "${words[@]}") - (( CURRENT += 1 )) - curcontext="${curcontext%:*:*}:ciel-repo-command-$line[1]:" - case $line[1] in - (refresh) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -'::PATH -- Path to the repository to refresh:_files' \ -&& ret=0 -;; -(help) -_arguments "${_arguments_options[@]}" : \ -":: :_ciel__repo__help_commands" \ -"*::: :->help" \ -&& ret=0 - - case $state in - (help) - words=($line[1] "${words[@]}") - (( CURRENT += 1 )) - curcontext="${curcontext%:*:*}:ciel-repo-help-command-$line[1]:" - case $line[1] in - (refresh) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(help) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; - esac - ;; -esac -;; - esac - ;; -esac -;; -(clean) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(diagnose) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -&& ret=0 -;; -(help) -_arguments "${_arguments_options[@]}" : \ -":: :_ciel__help_commands" \ -"*::: :->help" \ -&& ret=0 - - case $state in - (help) - words=($line[1] "${words[@]}") - (( CURRENT += 1 )) - curcontext="${curcontext%:*:*}:ciel-help-command-$line[1]:" - case $line[1] in - (version) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(list) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(new) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(farewell) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(load-os) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(update-os) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(instconf) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(config) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(load-tree) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(add) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(del) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(mount) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(boot) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(stop) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(down) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(rollback) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(commit) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(shell) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(run) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(build) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(repo) -_arguments "${_arguments_options[@]}" : \ -":: :_ciel__help__repo_commands" \ -"*::: :->repo" \ -&& ret=0 - - case $state in - (repo) - words=($line[1] "${words[@]}") - (( CURRENT += 1 )) - curcontext="${curcontext%:*:*}:ciel-help-repo-command-$line[1]:" - case $line[1] in - (refresh) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; - esac - ;; -esac -;; -(clean) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(diagnose) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(help) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; - esac - ;; -esac -;; - esac - ;; -esac -} - -(( $+functions[_ciel_commands] )) || -_ciel_commands() { - local commands; commands=( -'version:Display the version of CIEL!' \ -'list:List all instances in the workspace' \ -'new:Create a new CIEL! workspace' \ -'farewell:Remove everything related to CIEL!' \ -'load-os:Unpack OS tarball or fetch the latest BuildKit' \ -'update-os:Update the OS in the container' \ -'instconf:Configure instances' \ -'config:Configure workspace' \ -'load-tree:Clone abbs tree from git' \ -'add:Add a new instance' \ -'del:Remove one or all instance' \ -'mount:Mount one or all instance' \ -'boot:Start one or all instance' \ -'stop:Shutdown one or all instance' \ -'down:Shutdown and unmount one or all instance' \ -'rollback:Rollback one or all instance' \ -'commit:Commit changes onto the underlying base system' \ -'shell:Start an interactive shell or run a shell command' \ -'run:Run a command in the container' \ -'build:Build the packages using the specified instance' \ -'repo:Local repository maintenance' \ -'clean:Clean all the output directories and source cache directories' \ -'diagnose:Diagnose problems (hopefully)' \ -'help:Print this message or the help of the given subcommand(s)' \ - ) - _describe -t commands 'ciel commands' commands "$@" -} -(( $+functions[_ciel__add_commands] )) || -_ciel__add_commands() { - local commands; commands=() - _describe -t commands 'ciel add commands' commands "$@" -} -(( $+functions[_ciel__boot_commands] )) || -_ciel__boot_commands() { - local commands; commands=() - _describe -t commands 'ciel boot commands' commands "$@" -} -(( $+functions[_ciel__build_commands] )) || -_ciel__build_commands() { - local commands; commands=() - _describe -t commands 'ciel build commands' commands "$@" -} -(( $+functions[_ciel__clean_commands] )) || -_ciel__clean_commands() { - local commands; commands=() - _describe -t commands 'ciel clean commands' commands "$@" -} -(( $+functions[_ciel__commit_commands] )) || -_ciel__commit_commands() { - local commands; commands=() - _describe -t commands 'ciel commit commands' commands "$@" -} -(( $+functions[_ciel__config_commands] )) || -_ciel__config_commands() { - local commands; commands=() - _describe -t commands 'ciel config commands' commands "$@" -} -(( $+functions[_ciel__del_commands] )) || -_ciel__del_commands() { - local commands; commands=() - _describe -t commands 'ciel del commands' commands "$@" -} -(( $+functions[_ciel__diagnose_commands] )) || -_ciel__diagnose_commands() { - local commands; commands=() - _describe -t commands 'ciel diagnose commands' commands "$@" -} -(( $+functions[_ciel__down_commands] )) || -_ciel__down_commands() { - local commands; commands=() - _describe -t commands 'ciel down commands' commands "$@" -} -(( $+functions[_ciel__farewell_commands] )) || -_ciel__farewell_commands() { - local commands; commands=() - _describe -t commands 'ciel farewell commands' commands "$@" -} -(( $+functions[_ciel__help_commands] )) || -_ciel__help_commands() { - local commands; commands=( -'version:Display the version of CIEL!' \ -'list:List all instances in the workspace' \ -'new:Create a new CIEL! workspace' \ -'farewell:Remove everything related to CIEL!' \ -'load-os:Unpack OS tarball or fetch the latest BuildKit' \ -'update-os:Update the OS in the container' \ -'instconf:Configure instances' \ -'config:Configure workspace' \ -'load-tree:Clone abbs tree from git' \ -'add:Add a new instance' \ -'del:Remove one or all instance' \ -'mount:Mount one or all instance' \ -'boot:Start one or all instance' \ -'stop:Shutdown one or all instance' \ -'down:Shutdown and unmount one or all instance' \ -'rollback:Rollback one or all instance' \ -'commit:Commit changes onto the underlying base system' \ -'shell:Start an interactive shell or run a shell command' \ -'run:Run a command in the container' \ -'build:Build the packages using the specified instance' \ -'repo:Local repository maintenance' \ -'clean:Clean all the output directories and source cache directories' \ -'diagnose:Diagnose problems (hopefully)' \ -'help:Print this message or the help of the given subcommand(s)' \ - ) - _describe -t commands 'ciel help commands' commands "$@" -} -(( $+functions[_ciel__help__add_commands] )) || -_ciel__help__add_commands() { - local commands; commands=() - _describe -t commands 'ciel help add commands' commands "$@" -} -(( $+functions[_ciel__help__boot_commands] )) || -_ciel__help__boot_commands() { - local commands; commands=() - _describe -t commands 'ciel help boot commands' commands "$@" -} -(( $+functions[_ciel__help__build_commands] )) || -_ciel__help__build_commands() { - local commands; commands=() - _describe -t commands 'ciel help build commands' commands "$@" -} -(( $+functions[_ciel__help__clean_commands] )) || -_ciel__help__clean_commands() { - local commands; commands=() - _describe -t commands 'ciel help clean commands' commands "$@" -} -(( $+functions[_ciel__help__commit_commands] )) || -_ciel__help__commit_commands() { - local commands; commands=() - _describe -t commands 'ciel help commit commands' commands "$@" -} -(( $+functions[_ciel__help__config_commands] )) || -_ciel__help__config_commands() { - local commands; commands=() - _describe -t commands 'ciel help config commands' commands "$@" -} -(( $+functions[_ciel__help__del_commands] )) || -_ciel__help__del_commands() { - local commands; commands=() - _describe -t commands 'ciel help del commands' commands "$@" -} -(( $+functions[_ciel__help__diagnose_commands] )) || -_ciel__help__diagnose_commands() { - local commands; commands=() - _describe -t commands 'ciel help diagnose commands' commands "$@" -} -(( $+functions[_ciel__help__down_commands] )) || -_ciel__help__down_commands() { - local commands; commands=() - _describe -t commands 'ciel help down commands' commands "$@" -} -(( $+functions[_ciel__help__farewell_commands] )) || -_ciel__help__farewell_commands() { - local commands; commands=() - _describe -t commands 'ciel help farewell commands' commands "$@" -} -(( $+functions[_ciel__help__help_commands] )) || -_ciel__help__help_commands() { - local commands; commands=() - _describe -t commands 'ciel help help commands' commands "$@" -} -(( $+functions[_ciel__help__instconf_commands] )) || -_ciel__help__instconf_commands() { - local commands; commands=() - _describe -t commands 'ciel help instconf commands' commands "$@" -} -(( $+functions[_ciel__help__list_commands] )) || -_ciel__help__list_commands() { - local commands; commands=() - _describe -t commands 'ciel help list commands' commands "$@" -} -(( $+functions[_ciel__help__load-os_commands] )) || -_ciel__help__load-os_commands() { - local commands; commands=() - _describe -t commands 'ciel help load-os commands' commands "$@" -} -(( $+functions[_ciel__help__load-tree_commands] )) || -_ciel__help__load-tree_commands() { - local commands; commands=() - _describe -t commands 'ciel help load-tree commands' commands "$@" -} -(( $+functions[_ciel__help__mount_commands] )) || -_ciel__help__mount_commands() { - local commands; commands=() - _describe -t commands 'ciel help mount commands' commands "$@" -} -(( $+functions[_ciel__help__new_commands] )) || -_ciel__help__new_commands() { - local commands; commands=() - _describe -t commands 'ciel help new commands' commands "$@" -} -(( $+functions[_ciel__help__repo_commands] )) || -_ciel__help__repo_commands() { - local commands; commands=( -'refresh:Refresh the repository' \ - ) - _describe -t commands 'ciel help repo commands' commands "$@" -} -(( $+functions[_ciel__help__repo__refresh_commands] )) || -_ciel__help__repo__refresh_commands() { - local commands; commands=() - _describe -t commands 'ciel help repo refresh commands' commands "$@" -} -(( $+functions[_ciel__help__rollback_commands] )) || -_ciel__help__rollback_commands() { - local commands; commands=() - _describe -t commands 'ciel help rollback commands' commands "$@" -} -(( $+functions[_ciel__help__run_commands] )) || -_ciel__help__run_commands() { - local commands; commands=() - _describe -t commands 'ciel help run commands' commands "$@" -} -(( $+functions[_ciel__help__shell_commands] )) || -_ciel__help__shell_commands() { - local commands; commands=() - _describe -t commands 'ciel help shell commands' commands "$@" -} -(( $+functions[_ciel__help__stop_commands] )) || -_ciel__help__stop_commands() { - local commands; commands=() - _describe -t commands 'ciel help stop commands' commands "$@" -} -(( $+functions[_ciel__help__update-os_commands] )) || -_ciel__help__update-os_commands() { - local commands; commands=() - _describe -t commands 'ciel help update-os commands' commands "$@" -} -(( $+functions[_ciel__help__version_commands] )) || -_ciel__help__version_commands() { - local commands; commands=() - _describe -t commands 'ciel help version commands' commands "$@" -} -(( $+functions[_ciel__instconf_commands] )) || -_ciel__instconf_commands() { - local commands; commands=() - _describe -t commands 'ciel instconf commands' commands "$@" -} -(( $+functions[_ciel__list_commands] )) || -_ciel__list_commands() { - local commands; commands=() - _describe -t commands 'ciel list commands' commands "$@" -} -(( $+functions[_ciel__load-os_commands] )) || -_ciel__load-os_commands() { - local commands; commands=() - _describe -t commands 'ciel load-os commands' commands "$@" -} -(( $+functions[_ciel__load-tree_commands] )) || -_ciel__load-tree_commands() { - local commands; commands=() - _describe -t commands 'ciel load-tree commands' commands "$@" -} -(( $+functions[_ciel__mount_commands] )) || -_ciel__mount_commands() { - local commands; commands=() - _describe -t commands 'ciel mount commands' commands "$@" -} -(( $+functions[_ciel__new_commands] )) || -_ciel__new_commands() { - local commands; commands=() - _describe -t commands 'ciel new commands' commands "$@" -} -(( $+functions[_ciel__repo_commands] )) || -_ciel__repo_commands() { - local commands; commands=( -'refresh:Refresh the repository' \ -'help:Print this message or the help of the given subcommand(s)' \ - ) - _describe -t commands 'ciel repo commands' commands "$@" -} -(( $+functions[_ciel__repo__help_commands] )) || -_ciel__repo__help_commands() { - local commands; commands=( -'refresh:Refresh the repository' \ -'help:Print this message or the help of the given subcommand(s)' \ - ) - _describe -t commands 'ciel repo help commands' commands "$@" -} -(( $+functions[_ciel__repo__help__help_commands] )) || -_ciel__repo__help__help_commands() { - local commands; commands=() - _describe -t commands 'ciel repo help help commands' commands "$@" -} -(( $+functions[_ciel__repo__help__refresh_commands] )) || -_ciel__repo__help__refresh_commands() { - local commands; commands=() - _describe -t commands 'ciel repo help refresh commands' commands "$@" -} -(( $+functions[_ciel__repo__refresh_commands] )) || -_ciel__repo__refresh_commands() { - local commands; commands=() - _describe -t commands 'ciel repo refresh commands' commands "$@" -} -(( $+functions[_ciel__rollback_commands] )) || -_ciel__rollback_commands() { - local commands; commands=() - _describe -t commands 'ciel rollback commands' commands "$@" -} -(( $+functions[_ciel__run_commands] )) || -_ciel__run_commands() { - local commands; commands=() - _describe -t commands 'ciel run commands' commands "$@" -} -(( $+functions[_ciel__shell_commands] )) || -_ciel__shell_commands() { - local commands; commands=() - _describe -t commands 'ciel shell commands' commands "$@" -} -(( $+functions[_ciel__stop_commands] )) || -_ciel__stop_commands() { - local commands; commands=() - _describe -t commands 'ciel stop commands' commands "$@" -} -(( $+functions[_ciel__update-os_commands] )) || -_ciel__update-os_commands() { - local commands; commands=() - _describe -t commands 'ciel update-os commands' commands "$@" -} -(( $+functions[_ciel__version_commands] )) || -_ciel__version_commands() { - local commands; commands=() - _describe -t commands 'ciel version commands' commands "$@" -} - -if [ "$funcstack[1]" = "_ciel" ]; then - _ciel "$@" -else - compdef _ciel ciel -fi diff --git a/cli/completions/ciel.fish b/cli/completions/ciel.fish deleted file mode 100644 index d5ed212..0000000 --- a/cli/completions/ciel.fish +++ /dev/null @@ -1,225 +0,0 @@ -# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. -function __fish_ciel_global_optspecs - string join \n C= q/quiet h/help V/version -end - -function __fish_ciel_needs_command - # Figure out if the current invocation already has a command. - set -l cmd (commandline -opc) - set -e cmd[1] - argparse -s (__fish_ciel_global_optspecs) -- $cmd 2>/dev/null - or return - if set -q argv[1] - # Also print the command, so this can be used to figure out what it is. - echo $argv[1] - return 1 - end - return 0 -end - -function __fish_ciel_using_subcommand - set -l cmd (__fish_ciel_needs_command) - test -z "$cmd" - and return 1 - contains -- $cmd[1] $argv -end - -complete -c ciel -n "__fish_ciel_needs_command" -s C -d 'Set the CIEL! working directory' -r -complete -c ciel -n "__fish_ciel_needs_command" -s q -l quiet -d 'shhhhhh!' -complete -c ciel -n "__fish_ciel_needs_command" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_needs_command" -s V -l version -d 'Print version' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "version" -d 'Display the version of CIEL!' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "list" -d 'List all instances in the workspace' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "new" -d 'Create a new CIEL! workspace' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "farewell" -d 'Remove everything related to CIEL!' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "load-os" -d 'Unpack OS tarball or fetch the latest BuildKit' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "update-os" -d 'Update the OS in the container' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "instconf" -d 'Configure instances' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "config" -d 'Configure workspace' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "load-tree" -d 'Clone abbs tree from git' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "add" -d 'Add a new instance' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "del" -d 'Remove one or all instance' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "mount" -d 'Mount one or all instance' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "boot" -d 'Start one or all instance' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "stop" -d 'Shutdown one or all instance' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "down" -d 'Shutdown and unmount one or all instance' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "rollback" -d 'Rollback one or all instance' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "commit" -d 'Commit changes onto the underlying base system' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "shell" -d 'Start an interactive shell or run a shell command' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "run" -d 'Run a command in the container' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "build" -d 'Build the packages using the specified instance' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "repo" -d 'Local repository maintenance' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "clean" -d 'Clean all the output directories and source cache directories' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "diagnose" -d 'Diagnose problems (hopefully)' -complete -c ciel -n "__fish_ciel_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c ciel -n "__fish_ciel_using_subcommand version" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand list" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand new" -l rootfs -d 'Specify the tarball or squashfs to load after initialization' -r -complete -c ciel -n "__fish_ciel_using_subcommand new" -l sha256 -d 'Specify the SHA-256 checksum of OS tarball' -r -complete -c ciel -n "__fish_ciel_using_subcommand new" -s a -l arch -d 'Specify the architecture of the workspace' -r -complete -c ciel -n "__fish_ciel_using_subcommand new" -l tree -d 'URL to the abbs tree git repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand new" -s m -l maintainer -d 'Maintainer information' -r -complete -c ciel -n "__fish_ciel_using_subcommand new" -l dnssec -d 'Enable DNSSEC' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand new" -l local-repo -d 'Enable local package repository' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand new" -l source-cache -d 'Enable local source caches' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand new" -l branch-exclusive-output -d 'Use different OUTPUT directory for branches' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand new" -l volatile-mount -d 'Enable volatile mount' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand new" -l use-apt -d 'Force to use APT' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand new" -l add-repo -d 'Add an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand new" -l remove-repo -d 'Remove an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand new" -l add-nspawn-opt -d 'Add an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand new" -l remove-nspawn-opt -d 'Remove an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand new" -l no-load-os -d 'Don\'t load OS automatically after initialization' -complete -c ciel -n "__fish_ciel_using_subcommand new" -l no-load-tree -d 'Don\'t load abbs tree automatically after initialization' -complete -c ciel -n "__fish_ciel_using_subcommand new" -l unset-repo -d 'Remove all extra APT repository' -complete -c ciel -n "__fish_ciel_using_subcommand new" -l unset-nspawn-opt -d 'Remove all extra nspawn option' -complete -c ciel -n "__fish_ciel_using_subcommand new" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand farewell" -s f -d 'Force perform deletion without user confirmation' -complete -c ciel -n "__fish_ciel_using_subcommand farewell" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand load-os" -l sha256 -d 'Specify the SHA-256 checksum of OS tarball' -r -complete -c ciel -n "__fish_ciel_using_subcommand load-os" -s a -l arch -d 'Specify the target architecture for fetching OS tarball' -r -complete -c ciel -n "__fish_ciel_using_subcommand load-os" -s f -l force -d 'Force override the loaded system' -complete -c ciel -n "__fish_ciel_using_subcommand load-os" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l local-repo -d 'Enable local package repository' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l tmpfs -d 'Enable tmpfs' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l tmpfs-size -d 'Size of tmpfs to use, in MiB' -r -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l ro-tree -d 'Mount TREE as read-only' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l output -d 'Path to output directory' -r -F -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l add-repo -d 'Add an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l remove-repo -d 'Remove an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l add-nspawn-opt -d 'Add an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l remove-nspawn-opt -d 'Remove an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l force-use-apt -d 'Use apt to update-os' -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l unset-tmpfs-size -d 'Reset tmpfs size to default' -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l unset-output -d 'Use default output directory' -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l unset-repo -d 'Remove all extra APT repository' -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -l unset-nspawn-opt -d 'Remove all extra nspawn option' -complete -c ciel -n "__fish_ciel_using_subcommand update-os" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -s i -d 'Instance to be configured' -r -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l local-repo -d 'Enable local package repository' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l tmpfs -d 'Enable tmpfs' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l tmpfs-size -d 'Size of tmpfs to use, in MiB' -r -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l ro-tree -d 'Mount TREE as read-only' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l output -d 'Path to output directory' -r -F -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l add-repo -d 'Add an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l remove-repo -d 'Remove an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l add-nspawn-opt -d 'Add an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l remove-nspawn-opt -d 'Remove an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l force-no-rollback -d 'Do not rollback instances to apply configuration' -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l unset-tmpfs-size -d 'Reset tmpfs size to default' -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l unset-output -d 'Use default output directory' -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l unset-repo -d 'Remove all extra APT repository' -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -l unset-nspawn-opt -d 'Remove all extra nspawn option' -complete -c ciel -n "__fish_ciel_using_subcommand instconf" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand config" -s m -l maintainer -d 'Maintainer information' -r -complete -c ciel -n "__fish_ciel_using_subcommand config" -l dnssec -d 'Enable DNSSEC' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand config" -l local-repo -d 'Enable local package repository' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand config" -l source-cache -d 'Enable local source caches' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand config" -l branch-exclusive-output -d 'Use different OUTPUT directory for branches' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand config" -l volatile-mount -d 'Enable volatile mount' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand config" -l use-apt -d 'Force to use APT' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand config" -l add-repo -d 'Add an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand config" -l remove-repo -d 'Remove an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand config" -l add-nspawn-opt -d 'Add an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand config" -l remove-nspawn-opt -d 'Remove an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand config" -l force-no-rollback -d 'Do not rollback instances to apply configuration' -complete -c ciel -n "__fish_ciel_using_subcommand config" -l unset-repo -d 'Remove all extra APT repository' -complete -c ciel -n "__fish_ciel_using_subcommand config" -l unset-nspawn-opt -d 'Remove all extra nspawn option' -complete -c ciel -n "__fish_ciel_using_subcommand config" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand load-tree" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand add" -l local-repo -d 'Enable local package repository' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand add" -l tmpfs -d 'Enable tmpfs' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand add" -l tmpfs-size -d 'Size of tmpfs to use, in MiB' -r -complete -c ciel -n "__fish_ciel_using_subcommand add" -l ro-tree -d 'Mount TREE as read-only' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand add" -l output -d 'Path to output directory' -r -F -complete -c ciel -n "__fish_ciel_using_subcommand add" -l add-repo -d 'Add an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand add" -l remove-repo -d 'Remove an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand add" -l add-nspawn-opt -d 'Add an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand add" -l remove-nspawn-opt -d 'Remove an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand add" -l unset-tmpfs-size -d 'Reset tmpfs size to default' -complete -c ciel -n "__fish_ciel_using_subcommand add" -l unset-output -d 'Use default output directory' -complete -c ciel -n "__fish_ciel_using_subcommand add" -l unset-repo -d 'Remove all extra APT repository' -complete -c ciel -n "__fish_ciel_using_subcommand add" -l unset-nspawn-opt -d 'Remove all extra nspawn option' -complete -c ciel -n "__fish_ciel_using_subcommand add" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand del" -s a -l all -complete -c ciel -n "__fish_ciel_using_subcommand del" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand mount" -s a -l all -complete -c ciel -n "__fish_ciel_using_subcommand mount" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand boot" -s a -l all -complete -c ciel -n "__fish_ciel_using_subcommand boot" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand stop" -s a -l all -complete -c ciel -n "__fish_ciel_using_subcommand stop" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand down" -s a -l all -complete -c ciel -n "__fish_ciel_using_subcommand down" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand rollback" -s a -l all -complete -c ciel -n "__fish_ciel_using_subcommand rollback" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand commit" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand shell" -s i -d 'Instance to be used' -r -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l local-repo -d 'Enable local package repository' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l tmpfs -d 'Enable tmpfs' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l tmpfs-size -d 'Size of tmpfs to use, in MiB' -r -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l ro-tree -d 'Mount TREE as read-only' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l output -d 'Path to output directory' -r -F -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l add-repo -d 'Add an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l remove-repo -d 'Remove an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l add-nspawn-opt -d 'Add an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l remove-nspawn-opt -d 'Remove an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l unset-tmpfs-size -d 'Reset tmpfs size to default' -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l unset-output -d 'Use default output directory' -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l unset-repo -d 'Remove all extra APT repository' -complete -c ciel -n "__fish_ciel_using_subcommand shell" -l unset-nspawn-opt -d 'Remove all extra nspawn option' -complete -c ciel -n "__fish_ciel_using_subcommand shell" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand run" -s i -d 'Instance to run command in' -r -complete -c ciel -n "__fish_ciel_using_subcommand run" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand build" -s i -d 'Instance to be used' -r -complete -c ciel -n "__fish_ciel_using_subcommand build" -l local-repo -d 'Enable local package repository' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand build" -l tmpfs -d 'Enable tmpfs' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand build" -l tmpfs-size -d 'Size of tmpfs to use, in MiB' -r -complete -c ciel -n "__fish_ciel_using_subcommand build" -l ro-tree -d 'Mount TREE as read-only' -r -f -a "{true\t'',false\t''}" -complete -c ciel -n "__fish_ciel_using_subcommand build" -l output -d 'Path to output directory' -r -F -complete -c ciel -n "__fish_ciel_using_subcommand build" -l add-repo -d 'Add an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand build" -l remove-repo -d 'Remove an extra APT repository' -r -complete -c ciel -n "__fish_ciel_using_subcommand build" -l add-nspawn-opt -d 'Add an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand build" -l remove-nspawn-opt -d 'Remove an extra nspawn option' -r -complete -c ciel -n "__fish_ciel_using_subcommand build" -s c -l resume -d 'Resume from a Ciel checkpoint' -r -complete -c ciel -n "__fish_ciel_using_subcommand build" -l unset-tmpfs-size -d 'Reset tmpfs size to default' -complete -c ciel -n "__fish_ciel_using_subcommand build" -l unset-output -d 'Use default output directory' -complete -c ciel -n "__fish_ciel_using_subcommand build" -l unset-repo -d 'Remove all extra APT repository' -complete -c ciel -n "__fish_ciel_using_subcommand build" -l unset-nspawn-opt -d 'Remove all extra nspawn option' -complete -c ciel -n "__fish_ciel_using_subcommand build" -s g -d 'Fetch package sources only' -complete -c ciel -n "__fish_ciel_using_subcommand build" -l stage-select -d 'Select the starting point for a build' -complete -c ciel -n "__fish_ciel_using_subcommand build" -l always-discard -d 'Destory ephemeral containers if the build fails' -complete -c ciel -n "__fish_ciel_using_subcommand build" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand repo; and not __fish_seen_subcommand_from refresh help" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand repo; and not __fish_seen_subcommand_from refresh help" -f -a "refresh" -d 'Refresh the repository' -complete -c ciel -n "__fish_ciel_using_subcommand repo; and not __fish_seen_subcommand_from refresh help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c ciel -n "__fish_ciel_using_subcommand repo; and __fish_seen_subcommand_from refresh" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand repo; and __fish_seen_subcommand_from help" -f -a "refresh" -d 'Refresh the repository' -complete -c ciel -n "__fish_ciel_using_subcommand repo; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c ciel -n "__fish_ciel_using_subcommand clean" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand diagnose" -s h -l help -d 'Print help' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "version" -d 'Display the version of CIEL!' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "list" -d 'List all instances in the workspace' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "new" -d 'Create a new CIEL! workspace' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "farewell" -d 'Remove everything related to CIEL!' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "load-os" -d 'Unpack OS tarball or fetch the latest BuildKit' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "update-os" -d 'Update the OS in the container' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "instconf" -d 'Configure instances' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "config" -d 'Configure workspace' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "load-tree" -d 'Clone abbs tree from git' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "add" -d 'Add a new instance' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "del" -d 'Remove one or all instance' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "mount" -d 'Mount one or all instance' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "boot" -d 'Start one or all instance' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "stop" -d 'Shutdown one or all instance' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "down" -d 'Shutdown and unmount one or all instance' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "rollback" -d 'Rollback one or all instance' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "commit" -d 'Commit changes onto the underlying base system' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "shell" -d 'Start an interactive shell or run a shell command' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "run" -d 'Run a command in the container' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "build" -d 'Build the packages using the specified instance' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "repo" -d 'Local repository maintenance' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "clean" -d 'Clean all the output directories and source cache directories' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "diagnose" -d 'Diagnose problems (hopefully)' -complete -c ciel -n "__fish_ciel_using_subcommand help; and not __fish_seen_subcommand_from version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c ciel -n "__fish_ciel_using_subcommand help; and __fish_seen_subcommand_from repo" -f -a "refresh" -d 'Refresh the repository' diff --git a/cli/src/actions/build.rs b/cli/src/actions/build.rs deleted file mode 100644 index ef9fc23..0000000 --- a/cli/src/actions/build.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::{ - fs, - path::{Path, PathBuf}, - process::exit, -}; - -use anyhow::Result; -use ciel::{ - build::{BuildCheckPoint, BuildRequest}, - InstanceConfig, Workspace, -}; -use clap::ArgMatches; -use console::style; -use dialoguer::{theme::ColorfulTheme, Select}; -use log::info; -use walkdir::WalkDir; - -use crate::{config::patch_instance_config, utils::create_spinner}; - -pub fn clean_outputs() -> Result<()> { - let spinner = create_spinner("Removing output directories ...", 200); - for entry in WalkDir::new(".").max_depth(1) { - let entry = entry?; - if entry.file_type().is_dir() && entry.file_name().to_string_lossy().starts_with("OUTPUT-") - { - fs::remove_dir_all(entry.path())?; - } - } - if Path::new("SRCS").is_dir() { - fs::remove_dir_all("SRCS")?; - } - if Path::new("STATES").is_dir() { - fs::remove_dir_all("STATES")?; - } - spinner.finish_with_message("Done."); - - Ok(()) -} - -pub fn build_packages(args: &ArgMatches) -> Result<()> { - let ws = Workspace::current_dir()?; - - let mut ckpt = if let Some(file) = args.get_one::("resume") { - BuildCheckPoint::load(file)? - } else { - let mut req = BuildRequest::new( - args.get_many::("PACKAGES") - .unwrap() - .map(|s| s.to_owned()) - .collect(), - ); - req.fetch_only = args.get_flag("fetch-only"); - BuildCheckPoint::from(req, &ws)? - }; - - if args.get_flag("select") { - eprintln!("-*-* S T A G E\t\tS E L E C T *-*-"); - let selection = Select::with_theme(&ColorfulTheme::default()) - .default(0) - .with_prompt( - "Choose a package to start building from (left/right arrow keys to change pages)", - ) - .items(&ckpt.packages) - .interact()?; - ckpt.progress = selection; - } - - let res = if let Some(inst) = args.get_one::("INSTANCE") { - let inst = ws.instance(inst)?.open()?; - ckpt.execute(&inst) - } else { - let mut config = InstanceConfig::default(); - patch_instance_config(args, &mut config)?; - let inst = ws.ephemeral_container("build", config)?; - let result = ckpt.execute(&inst); - if result.is_err() && !args.get_flag("always-discard") { - info!( - "{}: keeping ephemeral container for debug", - inst.as_ns_name() - ); - _ = inst.leak(); - } else { - inst.discard()?; - } - result - }; - match res { - Ok(out) => { - eprintln!( - "{} - {} packages in {}", - style("BUILD SUCCESSFUL").bold().green(), - out.total_packages, - format_duration(out.time_elapsed) - ); - } - Err((ckpt, err)) => { - eprintln!("{} - {:?}", style("BUILD FAILED").bold().red(), err); - if let Some(ckpt) = ckpt { - if std::env::var("CIEL_NO_CHECKPOINT").is_err() { - dump_build_checkpoint(&ckpt)?; - } - } - println!("\x07"); // bell character - exit( - err.into_exit_status() - .and_then(|status| status.code()) - .unwrap_or(-1), - ) - } - } - Ok(()) -} - -fn format_duration(seconds: u64) -> String { - format!( - "{:02}:{:02}:{:02}", - seconds / 3600, - (seconds / 60) % 60, - seconds % 60 - ) -} - -fn dump_build_checkpoint(ckpt: &BuildCheckPoint) -> Result<()> { - let last_package = ckpt - .packages - .get(ckpt.progress) - .map_or("unknown".to_string(), |x| x.to_owned()); - let last_package = last_package.replace('/', "_"); - let current = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH)? - .as_secs(); - - fs::create_dir_all("STATES")?; - let path = PathBuf::from("STATES").join(format!("{}-{}.ciel-ckpt", last_package, current)); - ckpt.write(&path)?; - info!("Ciel created a check-point: {:?}", path); - - Ok(()) -} - -#[cfg(test)] -mod test { - use crate::actions::build::format_duration; - - #[test] - fn test_time_format() { - let test_dur = 3661; - assert_eq!(format_duration(test_dur), "01:01:01"); - } -} diff --git a/cli/src/actions/container.rs b/cli/src/actions/container.rs deleted file mode 100644 index 5557a48..0000000 --- a/cli/src/actions/container.rs +++ /dev/null @@ -1,121 +0,0 @@ -use anyhow::Result; -use ciel::{Instance, InstanceConfig, Workspace}; -use clap::ArgMatches; - -use crate::{config::patch_instance_config, utils::create_spinner}; - -pub fn add_instance(args: &ArgMatches) -> Result<()> { - let ws = Workspace::current_dir()?; - - let name = args.get_one::("INSTANCE").unwrap(); - let mut config = InstanceConfig::default(); - patch_instance_config(args, &mut config)?; - _ = ws.add_instance(name, config)?; - - Ok(()) -} - -#[inline] -fn one_or_more_instances(args: &ArgMatches, op: F) -> Result<()> -where - F: Fn(Instance) -> Result<()>, -{ - let ws = Workspace::current_dir()?; - - if args.get_flag("all") { - for inst in ws.instances()? { - op(inst)?; - } - } else { - let name = args.get_many::("INSTANCE").unwrap(); - for inst in name { - op(ws.instance(inst)?)?; - } - } - Ok(()) -} - -pub fn del_instance(args: &ArgMatches) -> Result<()> { - one_or_more_instances(args, |inst| Ok(inst.destroy()?)) -} - -pub fn mount_instance(args: &ArgMatches) -> Result<()> { - one_or_more_instances(args, |inst| Ok(inst.open()?.overlay_manager().mount()?)) -} - -pub fn boot_instance(args: &ArgMatches) -> Result<()> { - let spinner = create_spinner("Booting instance ...", 200); - one_or_more_instances(args, |inst| Ok(inst.open()?.boot()?))?; - spinner.finish_with_message("Done."); - Ok(()) -} - -pub fn stop_instance(args: &ArgMatches) -> Result<()> { - let spinner = create_spinner("Stopping instance ...", 200); - one_or_more_instances(args, |inst| Ok(inst.open()?.stop(false)?))?; - spinner.finish_with_message("Done."); - Ok(()) -} - -pub fn down_instance(args: &ArgMatches) -> Result<()> { - let spinner = create_spinner("Stopping instance ...", 200); - one_or_more_instances(args, |inst| Ok(inst.open()?.stop(true)?))?; - spinner.finish_with_message("Done."); - Ok(()) -} - -pub fn rollback_instance(args: &ArgMatches) -> Result<()> { - let spinner = create_spinner("Rolling back instance ...", 200); - one_or_more_instances(args, |inst| Ok(inst.open()?.rollback()?))?; - spinner.finish_with_message("Done."); - Ok(()) -} - -pub fn commit_instance(args: &ArgMatches) -> Result<()> { - let spinner = create_spinner("Commiting instance ...", 200); - let name = args.get_one::("INSTANCE").unwrap(); - let ws = Workspace::current_dir()?; - ws.commit(ws.instance(name)?.open()?)?; - spinner.finish_with_message("Done."); - Ok(()) -} - -pub fn run_in_container(args: &ArgMatches) -> Result<()> { - let name = args.get_one::("INSTANCE").unwrap(); - let commands = args.get_many::("COMMANDS").unwrap(); - - let ws = Workspace::current_dir()?; - let inst = ws.instance(name)?.open()?; - inst.boot()?; - inst.machine()?.exec(commands)?; - Ok(()) -} - -pub fn shell_run_in_container(args: &ArgMatches) -> Result<()> { - let commands = args - .get_many::("COMMANDS") - .map(|v| v.collect::>()) - .unwrap_or_default(); - let mut cmd = vec!["/usr/bin/bash".to_string()]; - if !commands.is_empty() { - cmd.push("-ec".to_string()); - cmd.push("exec \"$@\"".to_string()); - cmd.push("--".to_string()); - cmd.extend(commands.into_iter().map(|s| s.to_owned())); - } - - let ws = Workspace::current_dir()?; - if let Some(name) = args.get_one::("INSTANCE") { - let inst = ws.instance(name)?.open()?; - inst.boot()?; - inst.machine()?.exec(cmd)?; - } else { - let mut config = InstanceConfig::default(); - patch_instance_config(args, &mut config)?; - let inst = ws.ephemeral_container("shell", config)?; - inst.boot()?; - inst.machine()?.exec(cmd)?; - inst.discard()?; - } - Ok(()) -} diff --git a/cli/src/actions/mod.rs b/cli/src/actions/mod.rs deleted file mode 100644 index 9b82e4c..0000000 --- a/cli/src/actions/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod workspace; -pub use workspace::*; - -mod container; -pub use container::*; - -mod diagnose; -pub use diagnose::*; - -mod build; -pub use build::*; - -mod tree; -pub use tree::*; - -mod repo; -pub use repo::*; diff --git a/cli/src/actions/repo.rs b/cli/src/actions/repo.rs deleted file mode 100644 index 4574cdb..0000000 --- a/cli/src/actions/repo.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::path::PathBuf; - -use anyhow::Result; -use ciel::{SimpleAptRepository, Workspace}; -use log::info; - -pub fn refresh_repo(path: Option) -> Result<()> { - let ws = Workspace::current_dir()?; - info!("Refreshing local repository ..."); - SimpleAptRepository::new(path.unwrap_or_else(|| ws.output_directory())).refresh()?; - Ok(()) -} diff --git a/cli/src/actions/tree.rs b/cli/src/actions/tree.rs deleted file mode 100644 index 69961da..0000000 --- a/cli/src/actions/tree.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::path::Path; - -use anyhow::{bail, Result}; -use log::info; - -use crate::download::download_git; - -pub fn load_tree(url: String) -> Result<()> { - info!("Cloning abbs tree ..."); - let path = Path::new("TREE"); - if path.exists() { - bail!("TREE already exists") - } - download_git(&url, path)?; - Ok(()) -} diff --git a/cli/src/actions/workspace.rs b/cli/src/actions/workspace.rs deleted file mode 100644 index 22face8..0000000 --- a/cli/src/actions/workspace.rs +++ /dev/null @@ -1,301 +0,0 @@ -use std::{fs, path::PathBuf}; - -use anyhow::{anyhow, bail, Result}; -use ciel::{ContainerState, InstanceConfig, Workspace, WorkspaceConfig}; -use clap::ArgMatches; -use console::{style, user_attended}; -use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input}; -use log::info; - -use crate::{ - config::{ask_for_init_config, patch_instance_config, patch_workspace_config}, - download::{download_file, pick_latest_rootfs, CIEL_MAINLINE_ARCHS, CIEL_RETRO_ARCHS}, - logger::style_bool, - make_progress_bar, - utils::{self, get_host_arch_name}, -}; - -use super::load_tree; - -pub fn list_instances() -> Result<()> { - use std::io::Write; - use tabwriter::TabWriter; - - let ws = Workspace::current_dir()?; - - let mut formatter = TabWriter::new(std::io::stderr()); - writeln!(&mut formatter, "NAME\tMOUNTED\tSTARTED\tBOOTED")?; - - for inst in ws.instances()? { - let container = inst.open()?; - let state = container.state()?; - let (mounted, started, running) = match state { - ContainerState::Down => (false, false, false), - ContainerState::Mounted => (true, false, false), - ContainerState::Starting => (true, true, false), - ContainerState::Running => (true, true, true), - }; - let booted = { - if started { - style_bool(running) - } else { - // dim - "\x1b[2m-\x1b[0m" - } - }; - let mounted = style_bool(mounted); - let started = style_bool(started); - writeln!( - &mut formatter, - "{}\t{}\t{}\t{}", - inst.name(), - mounted, - started, - booted - )?; - } - formatter.flush()?; - - Ok(()) -} - -pub fn new_workspace(args: &ArgMatches) -> Result<()> { - let mut config = WorkspaceConfig::default(); - - let gitconfig = git2::Config::open_default()?; - if let Ok(name) = gitconfig.get_string("user.name") { - if let Ok(email) = gitconfig.get_string("user.email") { - config.maintainer = format!("{} <{}>", name, email); - } - } - let mut arch = args.get_one::("arch").cloned(); - - patch_workspace_config(args, &mut config)?; - if user_attended() { - if arch.is_none() { - arch = Some(ask_for_target_arch()?.to_owned()) - } - ask_for_init_config(&mut config)?; - } else { - info!("Running in unattended mode, using default configuration ..."); - } - Workspace::init(std::env::current_dir()?, config)?; - - if !args.get_flag("no-load-os") { - load_os( - args.get_one::("rootfs").cloned(), - args.get_one::("sha256").cloned(), - arch, - false, - )?; - } - - if !args.get_flag("no-load-tree") { - load_tree(args.get_one::("tree").unwrap().to_string())?; - } - - Ok(()) -} - -pub fn farewell(force: bool) -> Result<()> { - let ws = Workspace::current_dir()?; - if !user_attended() { - info!("Skipped user confirmation due to unattended mode"); - } else if !force { - let theme = ColorfulTheme::default(); - let delete = Confirm::with_theme(&theme) - .with_prompt("DELETE THIS CIEL WORKSPACE?") - .default(false) - .interact()?; - if !delete { - bail!("User cancelled") - } - info!( - "If you are absolutely sure, please type the following:\n{}", - style("Do as I say!").bold() - ); - if Input::::with_theme(&theme) - .with_prompt("Your turn") - .interact()? - != "Do as I say!" - { - bail!("User cancelled") - } - } - - info!("... as you wish. Commencing destruction ..."); - ws.destroy()?; - Ok(()) -} - -pub fn load_os( - url: Option, - sha256: Option, - arch: Option, - force: bool, -) -> Result<()> { - let ws = Workspace::current_dir()?; - - if ws.is_system_loaded() && !force { - if user_attended() { - let theme = ColorfulTheme::default(); - let confirm = Confirm::with_theme(&theme) - .with_prompt("Do you want to override the existing system?") - .default(false) - .interact()?; - if !confirm { - bail!("User cancelled") - } - } else { - bail!("A system is already loaded") - } - } - - let (url, mut sha256) = if let Some(url) = url { - (url, sha256) - } else { - let arch = if let Some(arch) = arch { - arch - } else { - get_host_arch_name()?.to_string() - }; - let rootfs = pick_latest_rootfs(&arch)?; - ( - format!("https://releases.aosc.io/{}", rootfs.path), - Some(rootfs.sha256sum), - ) - }; - - let path = PathBuf::from(&url); - let filename = path - .file_name() - .ok_or_else(|| anyhow!("Unable to convert path to string"))? - .to_str() - .ok_or_else(|| anyhow!("Unable to decode path string"))? - .to_owned(); - - let file = 'file: { - if url.starts_with("http://") || url.starts_with("https://") { - let dest = PathBuf::from(&filename); - if dest.exists() { - if let Some(expected_sha256) = &sha256 { - info!("Found local file with the same name, verifying checksum ..."); - let tarball = fs::File::open(&dest)?; - let checksum = utils::sha256sum(tarball)?; - if expected_sha256 == &checksum { - info!("Checksum verified, reusing local rootfs."); - sha256 = None; - break 'file dest; - } else { - info!( - "Checksum mismatch: expected {} but got {}", - expected_sha256, checksum - ); - } - } - } - info!("Downloading rootfs from {} ...", url); - download_file(&url, &dest)?; - dest - } else { - info!("Using rootfs from {}", url); - path - } - }; - - let total = file.metadata()?.len(); - - if let Some(sha256) = sha256 { - info!("Verifying tarball checksum ..."); - let tarball = fs::File::open(&file)?; - let checksum = utils::sha256sum(tarball)?; - if sha256 == checksum { - info!("Checksum verified."); - } else { - bail!( - "Checksum mismatch: expected {} but got {}", - sha256, - checksum - ); - } - } - - let progress_bar = indicatif::ProgressBar::new(total); - progress_bar.set_style( - indicatif::ProgressStyle::default_bar() - .template(make_progress_bar!("Extracting rootfs ...")) - .unwrap(), - ); - progress_bar.set_draw_target(indicatif::ProgressDrawTarget::stderr_with_hz(5)); - - let rootfs_dir = ws.system_rootfs(); - if rootfs_dir.exists() { - fs::remove_dir_all(&rootfs_dir).ok(); - fs::create_dir_all(&rootfs_dir)?; - } - - // detect if we are running in systemd-nspawn - // where /dev/console character device file cannot be created - // thus ignoring the error in extracting - let mut in_systemd_nspawn = false; - if let Ok(output) = std::process::Command::new("systemd-detect-virt").output() { - if let Ok("systemd-nspawn") = std::str::from_utf8(&output.stdout) { - in_systemd_nspawn = true; - } - } - - let res = if filename.ends_with(".tar.xz") { - let f = fs::File::open(&file)?; - utils::extract_tar_xz(progress_bar.wrap_read(f), &rootfs_dir) - } else if filename.ends_with(".sqfs") || filename.ends_with(".squashfs") { - utils::extract_squashfs(&file, &rootfs_dir, &progress_bar, total) - } else { - bail!("Unsupported rootfs format") - }; - - if !in_systemd_nspawn { - res? - } - progress_bar.finish_and_clear(); - Ok(()) -} - -pub fn update_os(args: &ArgMatches) -> Result<()> { - let ws = Workspace::current_dir()?; - - let mut config = InstanceConfig::default(); - config.use_local_repo = false; - patch_instance_config(args, &mut config)?; - - let inst = ws.ephemeral_container("update", config)?; - inst.boot()?; - if args.get_flag("force-use-apt") { - inst.machine()?.update_system(Some(true))?; - } else { - inst.machine()?.update_system(None)?; - } - ws.commit(&inst)?; - inst.discard()?; - - Ok(()) -} - -fn ask_for_target_arch() -> Result<&'static str> { - let mut all_archs: Vec<&'static str> = CIEL_MAINLINE_ARCHS.into(); - all_archs.append(&mut CIEL_RETRO_ARCHS.into()); - let host_arch = get_host_arch_name()?; - let default_arch_index = all_archs.iter().position(|a| *a == host_arch).unwrap(); - - let theme = ColorfulTheme::default(); - let prefixed_archs = CIEL_MAINLINE_ARCHS - .iter() - .map(|x| format!("mainline: {x}")) - .chain(CIEL_RETRO_ARCHS.iter().map(|x| format!("retro: {x}"))) - .collect::>(); - let chosen_index = FuzzySelect::with_theme(&theme) - .with_prompt("Target Architecture") - .default(default_arch_index) - .items(prefixed_archs.as_slice()) - .interact()?; - Ok(all_archs[chosen_index]) -} diff --git a/cli/src/cli.rs b/cli/src/cli.rs deleted file mode 100644 index a2bd611..0000000 --- a/cli/src/cli.rs +++ /dev/null @@ -1,484 +0,0 @@ -use anyhow::{anyhow, Result}; -use clap::{builder::ValueParser, value_parser, Arg, ArgAction, Command}; -use std::{ffi::OsStr, path::PathBuf}; - -pub const GIT_TREE_URL: &str = "https://github.com/AOSC-Dev/aosc-os-abbs.git"; - -/// List all the available plugins/helper scripts -fn list_helpers() -> Result> { - let exe_dir = std::env::current_exe().and_then(std::fs::canonicalize)?; - let exe_dir = exe_dir.parent().ok_or_else(|| anyhow!("Where am I?"))?; - let plugins_dir = exe_dir.join("../libexec/ciel-plugin/").read_dir()?; - let plugins = plugins_dir - .filter_map(|x| { - if let Ok(x) = x { - let path = x.path(); - let filename = path - .file_name() - .unwrap_or_else(|| OsStr::new("")) - .to_string_lossy(); - if path.is_file() && filename.starts_with("ciel-") { - return Some(filename.to_string()); - } - } - None - }) - .collect(); - - Ok(plugins) -} - -fn config_list(id: &str, name: &str, parser: ValueParser) -> [Arg; 3] { - [ - Arg::new(format!("add-{id}")) - .long(format!("add-{id}")) - .help(format!("Add an {name}")) - .value_name(id.to_owned()) - .value_parser(parser.clone()) - .required(false), - Arg::new(format!("remove-{id}")) - .long(format!("remove-{id}")) - .help(format!("Remove an {name}")) - .value_name(id.to_owned()) - .value_parser(parser) - .required(false), - Arg::new(format!("unset-{id}")) - .long(format!("unset-{id}")) - .help(format!("Remove all {name}")) - .action(ArgAction::SetTrue), - ] -} - -/// Build the CLI instance -pub fn build_cli() -> Command { - let instance_arg = Arg::new("INSTANCE") - .short('i') - .num_args(1) - .env("CIEL_INST") - .action(clap::ArgAction::Set); - let mut workspace_configs: Vec = vec![ - Arg::new("maintainer") - .long("maintainer") - .short('m') - .help("Maintainer information") - .value_parser(value_parser!(String)), - Arg::new("dnssec") - .long("dnssec") - .help("Enable DNSSEC") - .value_parser(value_parser!(bool)), - Arg::new("local-repo") - .long("local-repo") - .help("Enable local package repository") - .value_parser(value_parser!(bool)), - Arg::new("source-cache") - .long("source-cache") - .help("Enable local source caches") - .value_parser(value_parser!(bool)), - Arg::new("branch-exclusive-output") - .long("branch-exclusive-output") - .help("Use different OUTPUT directory for branches") - .value_parser(value_parser!(bool)), - Arg::new("volatile-mount") - .long("volatile-mount") - .help("Enable volatile mount") - .value_parser(value_parser!(bool)), - Arg::new("use-apt") - .long("use-apt") - .help("Force to use APT") - .value_parser(value_parser!(bool)), - ]; - workspace_configs.extend(config_list( - "repo", - "extra APT repository", - value_parser!(String), - )); - workspace_configs.extend(config_list( - "nspawn-opt", - "extra nspawn option", - value_parser!(String), - )); - let mut instance_configs = vec![ - Arg::new("local-repo") - .long("local-repo") - .help("Enable local package repository") - .value_parser(value_parser!(bool)), - // tmpfs - Arg::new("tmpfs") - .long("tmpfs") - .help("Enable tmpfs") - .value_parser(value_parser!(bool)), - Arg::new("tmpfs-size") - .long("tmpfs-size") - .help("Size of tmpfs to use, in MiB") - .value_parser(value_parser!(u64)), - Arg::new("unset-tmpfs-size") - .long("unset-tmpfs-size") - .help("Reset tmpfs size to default") - .action(ArgAction::SetTrue) - .conflicts_with("tmpfs-size"), - // read-write tree - Arg::new("ro-tree") - .long("ro-tree") - .help("Mount TREE as read-only") - .value_parser(value_parser!(bool)), - // custom output - Arg::new("output") - .long("output") - .value_parser(value_parser!(PathBuf)) - .help("Path to output directory"), - Arg::new("unset-output") - .long("unset-output") - .help("Use default output directory") - .action(ArgAction::SetTrue) - .conflicts_with("output"), - ]; - instance_configs.extend(config_list( - "repo", - "extra APT repository", - value_parser!(String), - )); - instance_configs.extend(config_list( - "nspawn-opt", - "extra nspawn option", - value_parser!(String), - )); - let one_or_more_instances = [ - Arg::new("INSTANCE") - .required(false) - .num_args(1..) - .env("CIEL_INST"), - Arg::new("all") - .short('a') - .long("all") - .action(ArgAction::SetTrue) - .required_unless_present("INSTANCE"), - ]; - - Command::new("ciel") - .version(env!("CARGO_PKG_VERSION")) - .about("CIEL! is a nspawn container manager") - .allow_external_subcommands(true) - .subcommand(Command::new("version").about("Display the version of CIEL!")) - .subcommand( - Command::new("list") - .alias("ls") - .about("List all instances in the workspace"), - ) - .subcommand( - Command::new("new") - .alias("init") - .arg( - Arg::new("no-load-os") - .long("no-load-os") - .action(ArgAction::SetTrue) - .help("Don't load OS automatically after initialization") - .conflicts_with_all(["rootfs", "sha256"]), - ) - .arg( - Arg::new("rootfs") - .num_args(1) - .long("rootfs") - .alias("from-tarball") - .help("Specify the tarball or squashfs to load after initialization"), - ) - .arg( - Arg::new("sha256") - .long("sha256") - .required(false) - .help("Specify the SHA-256 checksum of OS tarball"), - ) - .arg( - Arg::new("arch") - .short('a') - .long("arch") - .help("Specify the architecture of the workspace"), - ) - .arg( - Arg::new("no-load-tree") - .long("no-load-tree") - .action(ArgAction::SetTrue) - .help("Don't load abbs tree automatically after initialization") - .conflicts_with("tree"), - ) - .arg( - Arg::new("tree") - .long("tree") - .default_value(GIT_TREE_URL) - .help("URL to the abbs tree git repository"), - ) - .args( - workspace_configs - .iter() - .cloned() - .map(|arg| arg.required(false)), - ) - .about("Create a new CIEL! workspace"), - ) - .subcommand( - Command::new("farewell") - .alias("harakiri") - .about("Remove everything related to CIEL!") - .arg( - Arg::new("force") - .short('f') - .action(ArgAction::SetTrue) - .help("Force perform deletion without user confirmation"), - ), - ) - .subcommand( - Command::new("load-os") - .arg( - Arg::new("URL") - .required(false) - .help("URL or path to the tarball or squashfs"), - ) - .arg( - Arg::new("sha256") - .long("sha256") - .required(false) - .help("Specify the SHA-256 checksum of OS tarball"), - ) - .arg( - Arg::new("arch") - .short('a') - .long("arch") - .help("Specify the target architecture for fetching OS tarball"), - ) - .arg( - Arg::new("force") - .short('f') - .long("force") - .action(ArgAction::SetTrue) - .help("Force override the loaded system"), - ) - .about("Unpack OS tarball or fetch the latest BuildKit"), - ) - .subcommand( - Command::new("update-os") - .arg( - Arg::new("force-use-apt") - .long("force-use-apt") - .help("Use apt to update-os") - .action(ArgAction::SetTrue), - ) - .args(instance_configs.iter().cloned()) - .about("Update the OS in the container"), - ) - .subcommand( - Command::new("instconf") - .arg( - instance_arg - .clone() - .help("Instance to be configured") - .required(true), - ) - .arg( - Arg::new("force-no-rollback") - .long("force-no-rollback") - .action(ArgAction::SetTrue) - .help("Do not rollback instances to apply configuration"), - ) - .args(instance_configs.iter().cloned()) - .about("Configure instances"), - ) - .subcommand( - Command::new("config") - .arg( - Arg::new("force-no-rollback") - .long("force-no-rollback") - .action(ArgAction::SetTrue) - .help("Do not rollback instances to apply configuration"), - ) - .args(workspace_configs.iter().cloned()) - .about("Configure workspace"), - ) - .subcommand( - Command::new("load-tree") - .arg( - Arg::new("URL") - .default_value(GIT_TREE_URL) - .help("URL to the git repository"), - ) - .about("Clone abbs tree from git"), - ) - .subcommand( - Command::new("add") - .arg(Arg::new("INSTANCE").required(true)) - .args(instance_configs.iter().cloned()) - .about("Add a new instance"), - ) - .subcommand( - Command::new("del") - .alias("rm") - .args(&one_or_more_instances) - .about("Remove one or all instance"), - ) - .subcommand( - Command::new("mount") - .args(&one_or_more_instances) - .about("Mount one or all instance"), - ) - .subcommand( - Command::new("boot") - .args(&one_or_more_instances) - .about("Start one or all instance"), - ) - .subcommand( - Command::new("stop") - .args(&one_or_more_instances) - .about("Shutdown one or all instance"), - ) - .subcommand( - Command::new("down") - .alias("umount") - .args(&one_or_more_instances) - .about("Shutdown and unmount one or all instance"), - ) - .subcommand( - Command::new("rollback") - .alias("reset") - .args(&one_or_more_instances) - .about("Rollback one or all instance"), - ) - .subcommand( - Command::new("commit") - .arg(Arg::new("INSTANCE").env("CIEL_INST").required(true)) - .about("Commit changes onto the underlying base system"), - ) - .subcommand( - Command::new("shell") - .alias("sh") - .arg( - instance_arg - .clone() - .required(false) - .help("Instance to be used"), - ) - .args( - instance_configs - .iter() - .cloned() - .map(|arg| arg.conflicts_with("INSTANCE")), - ) - .arg(Arg::new("COMMANDS").required(false).num_args(1..)) - .about("Start an interactive shell or run a shell command"), - ) - .subcommand( - Command::new("run") - .alias("exec") - .arg(instance_arg.clone().help("Instance to run command in")) - .arg(Arg::new("COMMANDS").required(true).num_args(1..)) - .about("Run a command in the container"), - ) - .subcommand( - Command::new("build") - .arg( - instance_arg - .clone() - .required(false) - .help("Instance to be used"), - ) - .args( - instance_configs - .iter() - .cloned() - .map(|arg| arg.conflicts_with("INSTANCE")), - ) - .arg( - Arg::new("fetch-only") - .short('g') - .action(ArgAction::SetTrue) - .help("Fetch package sources only"), - ) - .arg( - Arg::new("resume") - .short('c') - .long("resume") - .alias("continue") - .num_args(1) - .help("Resume from a Ciel checkpoint") - .conflicts_with("fetch-only") - .conflicts_with("select"), - ) - .arg( - Arg::new("select") - .long("stage-select") - .action(ArgAction::SetTrue) - .help("Select the starting point for a build"), - ) - .arg( - Arg::new("always-discard") - .long("always-discard") - .action(ArgAction::SetTrue) - .conflicts_with("INSTANCE") - .help("Destory ephemeral containers if the build fails"), - ) - .arg( - Arg::new("PACKAGES") - .conflicts_with("resume") - .num_args(1..) - .required_unless_present("resume"), - ) - .about("Build the packages using the specified instance"), - ) - .subcommand( - Command::new("repo") - .arg_required_else_help(true) - .subcommands([Command::new("refresh") - .alias("init") - .about("Refresh the repository") - .arg( - Arg::new("PATH") - .required(false) - .value_parser(value_parser!(PathBuf)) - .help("Path to the repository to refresh"), - )]) - .alias("localrepo") - .about("Local repository maintenance"), - ) - .subcommand( - Command::new("clean") - .about("Clean all the output directories and source cache directories"), - ) - .subcommand( - Command::new("diagnose") - .alias("doctor") - .about("Diagnose problems (hopefully)"), - ) - .subcommands({ - let plugins = list_helpers(); - if let Ok(plugins) = plugins { - plugins - .iter() - .map(|plugin| { - let name = plugin.strip_prefix("ciel-").unwrap_or("???"); - Command::new(name.to_string()) - .arg( - Arg::new("COMMANDS") - .required(false) - .num_args(1..) - .help("Applet specific commands"), - ) - .about("") - }) - .collect() - } else { - vec![] - } - }) - .arg( - Arg::new("ciel-dir") - .short('C') - .value_name("DIR") - .default_value(".") - .env("CIEL_DIR") - .help("Set the CIEL! working directory"), - ) - .arg( - Arg::new("quiet") - .short('q') - .long("quiet") - .action(ArgAction::SetTrue) - .help("shhhhhh!"), - ) -} diff --git a/cli/src/config.rs b/cli/src/config.rs deleted file mode 100644 index c84f66a..0000000 --- a/cli/src/config.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::{fmt::Display, path::PathBuf}; - -use anyhow::Result; -use ciel::{InstanceConfig, Workspace, WorkspaceConfig}; -use clap::ArgMatches; -use dialoguer::{theme::ColorfulTheme, Confirm, Input}; -use log::info; - -use crate::utils::get_host_arch_name; - -#[inline] -fn config_list(args: &ArgMatches, id: &str, list: &mut Vec) -where - V: ToOwned + Display + PartialEq + Clone + Send + Sync + 'static, -{ - if args.get_flag(&format!("unset-{}", id)) { - list.clear(); - } - - if let Some(val) = args.get_one::(&format!("add-{}", id)) { - if !list.contains(val) { - list.push(val.to_owned()); - } - } - - if let Some(val) = args.get_one::(&format!("remove-{}", id)) { - if list.contains(val) { - let mut new_list = list.drain(0..).filter(|o| o != val).collect(); - list.append(&mut new_list); - } - } -} - -#[inline] -fn config_scalar(args: &ArgMatches, id: &str, val: &mut T) { - if let Some(new_val) = args.get_one::(id) { - *val = new_val.clone(); - } -} - -pub fn config_workspace(args: &ArgMatches) -> Result<()> { - let ws = Workspace::current_dir()?; - let mut config = ws.config(); - let old_config = config.clone(); - - patch_workspace_config(args, &mut config)?; - - if config != old_config { - info!("Applying new workspace configuration ..."); - if !args.get_flag("force-no-rollback") { - for inst in ws.instances()? { - inst.open()?.rollback()?; - } - } - } else { - info!("Nothing has been changed"); - } - - ws.set_config(config)?; - Ok(()) -} - -pub fn config_instance(instance: &str, args: &ArgMatches) -> Result<()> { - let ws = Workspace::current_dir()?; - let inst = ws.instance(instance)?; - let mut config = inst.config(); - let old_config = config.clone(); - - patch_instance_config(args, &mut config)?; - - if config != old_config { - info!("{}: applying new configurations ...", instance); - if !args.get_flag("force-no-rollback") { - inst.open()?.rollback()?; - } - } else { - info!("Nothing has been changed"); - } - inst.set_config(config)?; - Ok(()) -} - -/// Applies workspace configuration patches from [ArgMatches]. -pub fn patch_workspace_config(args: &ArgMatches, config: &mut WorkspaceConfig) -> Result<()> { - if let Some(maintainer) = args.get_one::("maintainer") { - if maintainer != &config.maintainer { - WorkspaceConfig::validate_maintainer(maintainer)?; - config.maintainer = maintainer.to_owned(); - } - } - - config_scalar(args, "dnssec", &mut config.dnssec); - config_list(args, "repo", &mut config.extra_apt_repos); - config_scalar(args, "local-repo", &mut config.use_local_repo); - config_scalar(args, "source-cache", &mut config.cache_sources); - config_list(args, "nspawn-opt", &mut config.extra_nspawn_options); - config_scalar( - args, - "branch-exclusive-output", - &mut config.branch_exclusive_output, - ); - config_scalar(args, "volatile-mount", &mut config.volatile_mount); - config_scalar(args, "use-apt", &mut config.use_apt); - - Ok(()) -} - -/// Applies instance configuration patches from [ArgMatches]. -pub fn patch_instance_config(args: &ArgMatches, config: &mut InstanceConfig) -> Result<()> { - if let Some(tmpfs) = args.get_one::("tmpfs") { - if *tmpfs && config.tmpfs.is_none() { - config.tmpfs = Some(Default::default()); - } - if !*tmpfs && config.tmpfs.is_some() { - config.tmpfs = None; - } - } - - if let Some(ref mut tmpfs) = &mut config.tmpfs { - if let Some(tmpfs_size) = args.get_one::("tmpfs-size") { - tmpfs.size = Some(*tmpfs_size as usize); - } else if args.get_flag("unset-tmpfs-size") { - tmpfs.size = None; - } - } - - config_list(args, "repo", &mut config.extra_apt_repos); - config_list(args, "nspawn-opt", &mut config.extra_nspawn_options); - config_scalar(args, "local-repo", &mut config.use_local_repo); - config_scalar(args, "ro-tree", &mut config.readonly_tree); - - if let Some(path) = args.get_one::("output") { - config.output = Some(path.to_owned()); - } else if args.get_flag("unset-output") { - config.output = None; - } - - Ok(()) -} - -/// Shows a series of prompts to let the user select the configurations -pub fn ask_for_init_config(config: &mut WorkspaceConfig) -> Result<()> { - let theme = ColorfulTheme::default(); - config.maintainer = Input::::with_theme(&theme) - .with_prompt("Maintainer") - .default(config.maintainer.to_owned()) - .validate_with(|s: &String| WorkspaceConfig::validate_maintainer(s.as_str())) - .interact_text()?; - config.cache_sources = Confirm::with_theme(&theme) - .with_prompt("Enable local sources caching") - .default(config.cache_sources) - .interact()?; - config.use_local_repo = Confirm::with_theme(&theme) - .with_prompt("Enable local packages repository") - .default(config.use_local_repo) - .interact()?; - config.branch_exclusive_output = Confirm::with_theme(&theme) - .with_prompt("Use different OUTPUT directories for different branches") - .default(config.branch_exclusive_output) - .interact()?; - - // FIXME: RISC-V build hosts is unreliable when using oma: random lock-ups - // during `oma refresh'. Disabling oma to workaround potential lock-ups. - if get_host_arch_name().map(|x| x != "riscv64").unwrap_or(true) { - info!("Ciel now uses oma as the default package manager for base system updating tasks."); - info!("You can choose whether to use oma instead of apt while configuring."); - config.use_apt = Confirm::with_theme(&theme) - .with_prompt("Use apt as package manager") - .default(config.use_apt) - .interact()?; - } - - Ok(()) -} diff --git a/cli/src/logger.rs b/cli/src/logger.rs deleted file mode 100644 index bd7d5f6..0000000 --- a/cli/src/logger.rs +++ /dev/null @@ -1,45 +0,0 @@ -use anyhow::Result; -use log::{Level, LevelFilter, Metadata, Record}; - -struct CielLogger; - -impl log::Log for CielLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= Level::Info - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - match record.level() { - Level::Error => { - eprint!("{} ", ::console::style("error:").red().bold()); - } - Level::Warn => { - eprint!("{} ", ::console::style("warn:").yellow().bold()); - } - Level::Info => { - eprint!("{} ", ::console::style("info:").cyan().bold()); - } - Level::Debug => todo!(), - Level::Trace => todo!(), - } - eprintln!("{}", record.args()); - } - } - - fn flush(&self) {} -} - -pub fn init() -> Result<()> { - log::set_boxed_logger(Box::new(CielLogger)).map(|()| log::set_max_level(LevelFilter::Info))?; - Ok(()) -} - -#[inline] -pub fn style_bool(pred: bool) -> &'static str { - if pred { - "\x1b[1m\x1b[32mYes\x1b[0m" - } else { - "\x1b[34mNo\x1b[0m" - } -} diff --git a/cli/src/main.rs b/cli/src/main.rs deleted file mode 100644 index dc6d425..0000000 --- a/cli/src/main.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::{ - path::PathBuf, - process::{exit, Command}, -}; - -use anyhow::{anyhow, Context, Result}; -use config::{config_instance, config_workspace}; -use console::style; -use log::{error, info}; -use nix::{ - sys::stat::{umask, Mode}, - unistd::geteuid, -}; - -mod actions; -mod cli; -mod config; -mod download; -mod logger; -mod utils; - -use actions::*; - -fn main() -> Result<()> { - // set umask to 022 to ensure correct permissions on rootfs - umask(Mode::S_IWGRP | Mode::S_IWOTH); - - // source .env file, ignore errors - _ = dotenvy::dotenv(); - - let cli = cli::build_cli(); - let version_string = cli.render_version(); - let args = cli.get_matches(); - - if !args.get_flag("quiet") { - logger::init()?; - } - - let subcommand = args.subcommand(); - if let Some(("version", _)) = subcommand { - println!("{}", version_string); - return Ok(()); - } - - if !geteuid().is_root() { - println!("Please run me as root!"); - std::process::exit(1); - } - - let workspace_dir = args.get_one::("ciel-dir").unwrap(); - let workspace_dir = match subcommand { - Some(("new", _)) => PathBuf::from(workspace_dir), - _ => { - let dir = utils::find_ciel_dir(workspace_dir) - .context("Error finding Ciel workspace directory")?; - info!( - "Selected workspace: {}", - style(dir.canonicalize()?.display()).cyan() - ); - dir - } - }; - std::env::set_current_dir(&workspace_dir)?; - - if let Some(subcommand) = subcommand { - let result = match subcommand { - ("list", _) => list_instances(), - ("new", args) => new_workspace(args), - ("farewell", args) => farewell(args.get_flag("force")), - ("load-os", args) => load_os( - args.get_one::("URL").cloned(), - args.get_one::("sha256").cloned(), - args.get_one::("arch").cloned(), - args.get_flag("force"), - ), - ("update-os", args) => update_os(args), - ("load-tree", args) => load_tree(args.get_one::("URL").unwrap().to_string()), - ("config", args) => config_workspace(args), - ("instconf", args) => { - config_instance(&args.get_one::("INSTANCE").unwrap(), args) - } - ("add", args) => add_instance(args), - ("del", args) => del_instance(args), - ("mount", args) => mount_instance(args), - ("boot", args) => boot_instance(args), - ("stop", args) => stop_instance(args), - ("down", args) => down_instance(args), - ("rollback", args) => rollback_instance(args), - ("commit", args) => commit_instance(args), - ("diagnose", _) => run_diagnose(), - ("clean", _) => clean_outputs(), - ("run", args) => run_in_container(args), - ("shell", args) => shell_run_in_container(args), - ("build", args) => build_packages(args), - ("repo", args) => match args.subcommand().unwrap() { - ("refresh", args) => refresh_repo(args.get_one::("PATH").cloned()), - (cmd, _) => Err(anyhow!("unknown command: `{}`.", cmd)), - }, - (cmd, args) => { - let exe_dir = std::env::current_exe()?; - let exe_dir = exe_dir.parent().expect("Where am I?"); - let plugin = exe_dir - .join("../libexec/ciel-plugin/") - .join(format!("ciel-{}", cmd)); - if !plugin.is_file() { - error!("unknown command: `{}`.", cmd); - exit(1); - } - let mut process = &mut Command::new(plugin); - if let Some(args) = args.get_many::("COMMANDS") { - process = process.args(args); - } - let status = process.status()?.code().unwrap(); - exit(status); - } - }; - if let Err(err) = result { - error!("{:?}", err); - exit(1); - } - Ok(()) - } else { - list_instances() - } -} diff --git a/cli/src/utils.rs b/cli/src/utils.rs deleted file mode 100644 index 39155c5..0000000 --- a/cli/src/utils.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::{ - io::Read, - os::unix::fs::MetadataExt, - path::{Path, PathBuf}, - sync::LazyLock, - time::Duration, -}; - -use anyhow::{bail, Result}; -use indicatif::ProgressBar; -use sha2::{Digest, Sha256}; -use unsquashfs_wrapper::Unsquashfs; - -#[macro_export] -macro_rules! make_progress_bar { - ($msg:expr) => { - concat!( - "{spinner} [{bar:25.cyan/blue}] ", - $msg, - " ({bytes_per_sec}, eta {eta})" - ) - }; -} - -/// Finds the Ciel workspace. -pub fn find_ciel_dir>(start: P) -> Result { - let start_path = std::fs::metadata(start.as_ref())?; - let start_dev = start_path.dev(); - let mut current_dir = start.as_ref().to_path_buf(); - loop { - if !current_dir.exists() { - bail!("Not a Ciel workspace: jit filesystem ceiling!") - } - let current_dev = current_dir.metadata()?.dev(); - if current_dev != start_dev { - bail!("Not a Ciel workspace: hit filesystem boundary!") - } - if current_dir.join(".ciel").is_dir() { - return Ok(current_dir); - } - current_dir = current_dir.join(".."); - } -} - -/// Gets host-machine architecture in AOSC specific style. -pub fn get_host_arch_name() -> Result<&'static str> { - #[cfg(not(target_arch = "powerpc64"))] - match std::env::consts::ARCH { - "x86_64" => Ok("amd64"), - "x86" => Ok("i486"), - "powerpc" => Ok("powerpc"), - "aarch64" => Ok("arm64"), - "mips64" => Ok("loongson3"), - "riscv64" => Ok("riscv64"), - "loongarch64" => Ok("loongarch64"), - _ => bail!("Unrecognized host architecture"), - } - - #[cfg(target_arch = "powerpc64")] - { - let mut endian: nix::libc::c_int = -1; - let result = unsafe { - nix::libc::prctl( - nix::libc::PR_GET_ENDIAN, - &mut endian as *mut nix::libc::c_int, - ) - }; - if result < 0 { - bail!("Failed to get host endian"); - } - match endian { - nix::libc::PR_ENDIAN_LITTLE | nix::libc::PR_ENDIAN_PPC_LITTLE => Ok("ppc64el"), - nix::libc::PR_ENDIAN_BIG => Ok("ppc64"), - _ => bail!("Unrecognized host architecture"), - } - } -} - -/// Calculate the SHA-256 checksum of the given stream -pub fn sha256sum(mut reader: R) -> Result { - let mut hasher = Sha256::new(); - std::io::copy(&mut reader, &mut hasher)?; - Ok(format!("{:x}", hasher.finalize())) -} - -/// Extract the given .tar.xz stream and preserve all the file attributes -pub fn extract_tar_xz(reader: R, path: &Path) -> Result<()> { - let decompress = xz2::read::XzDecoder::new(reader); - let mut tar_processor = tar::Archive::new(decompress); - tar_processor.set_unpack_xattrs(true); - tar_processor.set_preserve_permissions(true); - tar_processor.unpack(path)?; - - Ok(()) -} - -/// Extract the given .squashfs -pub fn extract_squashfs(path: &Path, dist_dir: &Path, pb: &ProgressBar, total: u64) -> Result<()> { - let unsquashfs = Unsquashfs::default(); - - unsquashfs.extract(path, dist_dir, None, move |c| { - pb.set_position(total * c as u64 / 100); - })?; - - Ok(()) -} - -static SPINNER_STYLE: LazyLock = LazyLock::new(|| { - indicatif::ProgressStyle::default_spinner() - .tick_chars("⠋⠙⠸⠴⠦⠇ ") - .template("{spinner:.green} {wide_msg}") - .unwrap() -}); - -pub fn create_spinner(msg: &'static str, tick_rate: u64) -> indicatif::ProgressBar { - let spinner = indicatif::ProgressBar::new_spinner().with_style(SPINNER_STYLE.clone()); - spinner.set_message(msg); - spinner.enable_steady_tick(Duration::from_millis(tick_rate)); - spinner -} diff --git a/completions/_ciel b/completions/_ciel new file mode 100644 index 0000000..f462e8a --- /dev/null +++ b/completions/_ciel @@ -0,0 +1,771 @@ +#compdef ciel + +autoload -U is-at-least + +_ciel() { + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext="$curcontext" state line + _arguments "${_arguments_options[@]}" \ +'-C+[Set the CIEL! working directory]:DIR: ' \ +'-b[Batch mode, no input required]' \ +'--batch[Batch mode, no input required]' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'-V[Print version information]' \ +'--version[Print version information]' \ +":: :_ciel_commands" \ +"*::: :->ciel" \ +&& ret=0 + case $state in + (ciel) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ciel-command-$line[1]:" + case $line[1] in + (version) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(init) +_arguments "${_arguments_options[@]}" \ +'--upgrade[Upgrade Ciel workspace from an older version]' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(load-os) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'::url -- URL or path to the tarball:' \ +&& ret=0 +;; +(update-os) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(load-tree) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'::url -- URL to the git repository:' \ +&& ret=0 +;; +(update-tree) +_arguments "${_arguments_options[@]}" \ +'-r+[Rebase the specified branch from the updated upstream]: : ' \ +'--rebase=[Rebase the specified branch from the updated upstream]: : ' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'::branch -- Branch to switch to:' \ +&& ret=0 +;; +(new) +_arguments "${_arguments_options[@]}" \ +'--from-tarball=[Create a new workspace from the specified tarball]: : ' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(list) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(add) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +':INSTANCE:' \ +&& ret=0 +;; +(del) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +':INSTANCE:' \ +&& ret=0 +;; +(shell) +_arguments "${_arguments_options[@]}" \ +'-i+[Instance to be used]: : ' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'*::COMMANDS:' \ +&& ret=0 +;; +(run) +_arguments "${_arguments_options[@]}" \ +'-i+[Instance to run command in]: : ' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'*::COMMANDS:' \ +&& ret=0 +;; +(config) +_arguments "${_arguments_options[@]}" \ +'-i+[Instance to be configured]: : ' \ +'(-i)-g[Configure base system instead of an instance]' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(commit) +_arguments "${_arguments_options[@]}" \ +'-i+[Instance to be committed]: : ' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(doctor) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(build) +_arguments "${_arguments_options[@]}" \ +'-i+[Instance to build in]: : ' \ +'(--stage-select)-c+[Continue from a Ciel checkpoint]: : ' \ +'(--stage-select)--resume=[Continue from a Ciel checkpoint]: : ' \ +'--stage-select=[Select the starting point for a build]' \ +'-g[Fetch source packages only]' \ +'-x[Disable network in the container during the build]' \ +'--offline[Disable network in the container during the build]' \ +'-2[Use stage 2 mode instead of the regular build mode]' \ +'--stage2[Use stage 2 mode instead of the regular build mode]' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'*::PACKAGES:' \ +&& ret=0 +;; +(rollback) +_arguments "${_arguments_options[@]}" \ +'-i+[Instance to be rolled back]: : ' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(down) +_arguments "${_arguments_options[@]}" \ +'-i+[Instance to be un-mounted]: : ' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(stop) +_arguments "${_arguments_options[@]}" \ +'-i+[Instance to be stopped]: : ' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(mount) +_arguments "${_arguments_options[@]}" \ +'-i+[Instance to be mounted]: : ' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(farewell) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(repo) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +":: :_ciel__repo_commands" \ +"*::: :->repo" \ +&& ret=0 + + case $state in + (repo) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ciel-repo-command-$line[1]:" + case $line[1] in + (refresh) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(init) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +':INSTANCE:' \ +&& ret=0 +;; +(deinit) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" \ +":: :_ciel__repo__help_commands" \ +"*::: :->help" \ +&& ret=0 + + case $state in + (help) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ciel-repo-help-command-$line[1]:" + case $line[1] in + (refresh) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(init) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(deinit) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; + esac + ;; +esac +;; + esac + ;; +esac +;; +(clean) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" \ +":: :_ciel__help_commands" \ +"*::: :->help" \ +&& ret=0 + + case $state in + (help) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ciel-help-command-$line[1]:" + case $line[1] in + (version) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(init) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(load-os) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(update-os) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(load-tree) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(update-tree) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(new) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(list) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(add) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(del) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(shell) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(run) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(config) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(commit) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(doctor) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(build) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(rollback) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(down) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(stop) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(mount) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(farewell) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(repo) +_arguments "${_arguments_options[@]}" \ +":: :_ciel__help__repo_commands" \ +"*::: :->repo" \ +&& ret=0 + + case $state in + (repo) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:ciel-help-repo-command-$line[1]:" + case $line[1] in + (refresh) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(init) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(deinit) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; + esac + ;; +esac +;; +(clean) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" \ +&& ret=0 +;; + esac + ;; +esac +;; + esac + ;; +esac +} + +(( $+functions[_ciel_commands] )) || +_ciel_commands() { + local commands; commands=( +'version:Display the version of CIEL!' \ +'init:Initialize the work directory' \ +'load-os:Unpack OS tarball or fetch the latest BuildKit from the repository' \ +'update-os:Update the OS in the container' \ +'load-tree:Clone package tree from the link provided or AOSC OS ABBS main repository' \ +'update-tree:Update the existing ABBS tree (fetch only) and optionally switch to a different branch' \ +'new:Create a new CIEL workspace' \ +'list:List all the instances under the specified working directory' \ +'add:Add a new instance' \ +'del:Remove an instance' \ +'shell:Start an interactive shell' \ +'run:Lower-level version of '\''shell'\'', without login environment, without sourcing ~/.bash_profile' \ +'config:Configure system and toolchain for building interactively' \ +'commit:Commit changes onto the shared underlying OS' \ +'doctor:Diagnose problems (hopefully)' \ +'build:Build the packages using the specified instance' \ +'rollback:Rollback all or specified instance' \ +'down:Shutdown and unmount all or one instance' \ +'stop:Shuts down an instance' \ +'mount:Mount all or specified instance' \ +'farewell:Remove everything related to CIEL!' \ +'repo:Local repository operations' \ +'clean:Clean all the output directories and source cache directories' \ +'help:Print this message or the help of the given subcommand(s)' \ + ) + _describe -t commands 'ciel commands' commands "$@" +} +(( $+functions[_ciel__add_commands] )) || +_ciel__add_commands() { + local commands; commands=() + _describe -t commands 'ciel add commands' commands "$@" +} +(( $+functions[_ciel__help__add_commands] )) || +_ciel__help__add_commands() { + local commands; commands=() + _describe -t commands 'ciel help add commands' commands "$@" +} +(( $+functions[_ciel__build_commands] )) || +_ciel__build_commands() { + local commands; commands=() + _describe -t commands 'ciel build commands' commands "$@" +} +(( $+functions[_ciel__help__build_commands] )) || +_ciel__help__build_commands() { + local commands; commands=() + _describe -t commands 'ciel help build commands' commands "$@" +} +(( $+functions[_ciel__clean_commands] )) || +_ciel__clean_commands() { + local commands; commands=() + _describe -t commands 'ciel clean commands' commands "$@" +} +(( $+functions[_ciel__help__clean_commands] )) || +_ciel__help__clean_commands() { + local commands; commands=() + _describe -t commands 'ciel help clean commands' commands "$@" +} +(( $+functions[_ciel__commit_commands] )) || +_ciel__commit_commands() { + local commands; commands=() + _describe -t commands 'ciel commit commands' commands "$@" +} +(( $+functions[_ciel__help__commit_commands] )) || +_ciel__help__commit_commands() { + local commands; commands=() + _describe -t commands 'ciel help commit commands' commands "$@" +} +(( $+functions[_ciel__config_commands] )) || +_ciel__config_commands() { + local commands; commands=() + _describe -t commands 'ciel config commands' commands "$@" +} +(( $+functions[_ciel__help__config_commands] )) || +_ciel__help__config_commands() { + local commands; commands=() + _describe -t commands 'ciel help config commands' commands "$@" +} +(( $+functions[_ciel__help__repo__deinit_commands] )) || +_ciel__help__repo__deinit_commands() { + local commands; commands=() + _describe -t commands 'ciel help repo deinit commands' commands "$@" +} +(( $+functions[_ciel__repo__deinit_commands] )) || +_ciel__repo__deinit_commands() { + local commands; commands=() + _describe -t commands 'ciel repo deinit commands' commands "$@" +} +(( $+functions[_ciel__repo__help__deinit_commands] )) || +_ciel__repo__help__deinit_commands() { + local commands; commands=() + _describe -t commands 'ciel repo help deinit commands' commands "$@" +} +(( $+functions[_ciel__del_commands] )) || +_ciel__del_commands() { + local commands; commands=() + _describe -t commands 'ciel del commands' commands "$@" +} +(( $+functions[_ciel__help__del_commands] )) || +_ciel__help__del_commands() { + local commands; commands=() + _describe -t commands 'ciel help del commands' commands "$@" +} +(( $+functions[_ciel__doctor_commands] )) || +_ciel__doctor_commands() { + local commands; commands=() + _describe -t commands 'ciel doctor commands' commands "$@" +} +(( $+functions[_ciel__help__doctor_commands] )) || +_ciel__help__doctor_commands() { + local commands; commands=() + _describe -t commands 'ciel help doctor commands' commands "$@" +} +(( $+functions[_ciel__down_commands] )) || +_ciel__down_commands() { + local commands; commands=() + _describe -t commands 'ciel down commands' commands "$@" +} +(( $+functions[_ciel__help__down_commands] )) || +_ciel__help__down_commands() { + local commands; commands=() + _describe -t commands 'ciel help down commands' commands "$@" +} +(( $+functions[_ciel__farewell_commands] )) || +_ciel__farewell_commands() { + local commands; commands=() + _describe -t commands 'ciel farewell commands' commands "$@" +} +(( $+functions[_ciel__help__farewell_commands] )) || +_ciel__help__farewell_commands() { + local commands; commands=() + _describe -t commands 'ciel help farewell commands' commands "$@" +} +(( $+functions[_ciel__help_commands] )) || +_ciel__help_commands() { + local commands; commands=( +'version:Display the version of CIEL!' \ +'init:Initialize the work directory' \ +'load-os:Unpack OS tarball or fetch the latest BuildKit from the repository' \ +'update-os:Update the OS in the container' \ +'load-tree:Clone package tree from the link provided or AOSC OS ABBS main repository' \ +'update-tree:Update the existing ABBS tree (fetch only) and optionally switch to a different branch' \ +'new:Create a new CIEL workspace' \ +'list:List all the instances under the specified working directory' \ +'add:Add a new instance' \ +'del:Remove an instance' \ +'shell:Start an interactive shell' \ +'run:Lower-level version of '\''shell'\'', without login environment, without sourcing ~/.bash_profile' \ +'config:Configure system and toolchain for building interactively' \ +'commit:Commit changes onto the shared underlying OS' \ +'doctor:Diagnose problems (hopefully)' \ +'build:Build the packages using the specified instance' \ +'rollback:Rollback all or specified instance' \ +'down:Shutdown and unmount all or one instance' \ +'stop:Shuts down an instance' \ +'mount:Mount all or specified instance' \ +'farewell:Remove everything related to CIEL!' \ +'repo:Local repository operations' \ +'clean:Clean all the output directories and source cache directories' \ +'help:Print this message or the help of the given subcommand(s)' \ + ) + _describe -t commands 'ciel help commands' commands "$@" +} +(( $+functions[_ciel__help__help_commands] )) || +_ciel__help__help_commands() { + local commands; commands=() + _describe -t commands 'ciel help help commands' commands "$@" +} +(( $+functions[_ciel__repo__help_commands] )) || +_ciel__repo__help_commands() { + local commands; commands=( +'refresh:Refresh the repository' \ +'init:Initialize the repository' \ +'deinit:Uninitialize the repository' \ +'help:Print this message or the help of the given subcommand(s)' \ + ) + _describe -t commands 'ciel repo help commands' commands "$@" +} +(( $+functions[_ciel__repo__help__help_commands] )) || +_ciel__repo__help__help_commands() { + local commands; commands=() + _describe -t commands 'ciel repo help help commands' commands "$@" +} +(( $+functions[_ciel__help__init_commands] )) || +_ciel__help__init_commands() { + local commands; commands=() + _describe -t commands 'ciel help init commands' commands "$@" +} +(( $+functions[_ciel__help__repo__init_commands] )) || +_ciel__help__repo__init_commands() { + local commands; commands=() + _describe -t commands 'ciel help repo init commands' commands "$@" +} +(( $+functions[_ciel__init_commands] )) || +_ciel__init_commands() { + local commands; commands=() + _describe -t commands 'ciel init commands' commands "$@" +} +(( $+functions[_ciel__repo__help__init_commands] )) || +_ciel__repo__help__init_commands() { + local commands; commands=() + _describe -t commands 'ciel repo help init commands' commands "$@" +} +(( $+functions[_ciel__repo__init_commands] )) || +_ciel__repo__init_commands() { + local commands; commands=() + _describe -t commands 'ciel repo init commands' commands "$@" +} +(( $+functions[_ciel__help__list_commands] )) || +_ciel__help__list_commands() { + local commands; commands=() + _describe -t commands 'ciel help list commands' commands "$@" +} +(( $+functions[_ciel__list_commands] )) || +_ciel__list_commands() { + local commands; commands=() + _describe -t commands 'ciel list commands' commands "$@" +} +(( $+functions[_ciel__help__load-os_commands] )) || +_ciel__help__load-os_commands() { + local commands; commands=() + _describe -t commands 'ciel help load-os commands' commands "$@" +} +(( $+functions[_ciel__load-os_commands] )) || +_ciel__load-os_commands() { + local commands; commands=() + _describe -t commands 'ciel load-os commands' commands "$@" +} +(( $+functions[_ciel__help__load-tree_commands] )) || +_ciel__help__load-tree_commands() { + local commands; commands=() + _describe -t commands 'ciel help load-tree commands' commands "$@" +} +(( $+functions[_ciel__load-tree_commands] )) || +_ciel__load-tree_commands() { + local commands; commands=() + _describe -t commands 'ciel load-tree commands' commands "$@" +} +(( $+functions[_ciel__help__mount_commands] )) || +_ciel__help__mount_commands() { + local commands; commands=() + _describe -t commands 'ciel help mount commands' commands "$@" +} +(( $+functions[_ciel__mount_commands] )) || +_ciel__mount_commands() { + local commands; commands=() + _describe -t commands 'ciel mount commands' commands "$@" +} +(( $+functions[_ciel__help__new_commands] )) || +_ciel__help__new_commands() { + local commands; commands=() + _describe -t commands 'ciel help new commands' commands "$@" +} +(( $+functions[_ciel__new_commands] )) || +_ciel__new_commands() { + local commands; commands=() + _describe -t commands 'ciel new commands' commands "$@" +} +(( $+functions[_ciel__help__repo__refresh_commands] )) || +_ciel__help__repo__refresh_commands() { + local commands; commands=() + _describe -t commands 'ciel help repo refresh commands' commands "$@" +} +(( $+functions[_ciel__repo__help__refresh_commands] )) || +_ciel__repo__help__refresh_commands() { + local commands; commands=() + _describe -t commands 'ciel repo help refresh commands' commands "$@" +} +(( $+functions[_ciel__repo__refresh_commands] )) || +_ciel__repo__refresh_commands() { + local commands; commands=() + _describe -t commands 'ciel repo refresh commands' commands "$@" +} +(( $+functions[_ciel__help__repo_commands] )) || +_ciel__help__repo_commands() { + local commands; commands=( +'refresh:Refresh the repository' \ +'init:Initialize the repository' \ +'deinit:Uninitialize the repository' \ + ) + _describe -t commands 'ciel help repo commands' commands "$@" +} +(( $+functions[_ciel__repo_commands] )) || +_ciel__repo_commands() { + local commands; commands=( +'refresh:Refresh the repository' \ +'init:Initialize the repository' \ +'deinit:Uninitialize the repository' \ +'help:Print this message or the help of the given subcommand(s)' \ + ) + _describe -t commands 'ciel repo commands' commands "$@" +} +(( $+functions[_ciel__help__rollback_commands] )) || +_ciel__help__rollback_commands() { + local commands; commands=() + _describe -t commands 'ciel help rollback commands' commands "$@" +} +(( $+functions[_ciel__rollback_commands] )) || +_ciel__rollback_commands() { + local commands; commands=() + _describe -t commands 'ciel rollback commands' commands "$@" +} +(( $+functions[_ciel__help__run_commands] )) || +_ciel__help__run_commands() { + local commands; commands=() + _describe -t commands 'ciel help run commands' commands "$@" +} +(( $+functions[_ciel__run_commands] )) || +_ciel__run_commands() { + local commands; commands=() + _describe -t commands 'ciel run commands' commands "$@" +} +(( $+functions[_ciel__help__shell_commands] )) || +_ciel__help__shell_commands() { + local commands; commands=() + _describe -t commands 'ciel help shell commands' commands "$@" +} +(( $+functions[_ciel__shell_commands] )) || +_ciel__shell_commands() { + local commands; commands=() + _describe -t commands 'ciel shell commands' commands "$@" +} +(( $+functions[_ciel__help__stop_commands] )) || +_ciel__help__stop_commands() { + local commands; commands=() + _describe -t commands 'ciel help stop commands' commands "$@" +} +(( $+functions[_ciel__stop_commands] )) || +_ciel__stop_commands() { + local commands; commands=() + _describe -t commands 'ciel stop commands' commands "$@" +} +(( $+functions[_ciel__help__update-os_commands] )) || +_ciel__help__update-os_commands() { + local commands; commands=() + _describe -t commands 'ciel help update-os commands' commands "$@" +} +(( $+functions[_ciel__update-os_commands] )) || +_ciel__update-os_commands() { + local commands; commands=() + _describe -t commands 'ciel update-os commands' commands "$@" +} +(( $+functions[_ciel__help__update-tree_commands] )) || +_ciel__help__update-tree_commands() { + local commands; commands=() + _describe -t commands 'ciel help update-tree commands' commands "$@" +} +(( $+functions[_ciel__update-tree_commands] )) || +_ciel__update-tree_commands() { + local commands; commands=() + _describe -t commands 'ciel update-tree commands' commands "$@" +} +(( $+functions[_ciel__help__version_commands] )) || +_ciel__help__version_commands() { + local commands; commands=() + _describe -t commands 'ciel help version commands' commands "$@" +} +(( $+functions[_ciel__version_commands] )) || +_ciel__version_commands() { + local commands; commands=() + _describe -t commands 'ciel version commands' commands "$@" +} + +_ciel "$@" diff --git a/cli/completions/ciel.bash b/completions/ciel.bash similarity index 67% rename from cli/completions/ciel.bash rename to completions/ciel.bash index 4d957a7..e5efb5f 100644 --- a/cli/completions/ciel.bash +++ b/completions/ciel.bash @@ -1,5 +1,42 @@ +_ciel_list_instances() { + local workdir="$(_ciel_find_ciel_workdir)" + [ -d "$workdir/".ciel/container/instances ] || return + find "$workdir/".ciel/container/instances -maxdepth 1 -mindepth 1 -type d -printf '%f\n' +} + +_ciel_find_ciel_workdir() { + local cur="$PWD" + while [ "$cur" != '/' ]; do + [ -d "$cur/".ciel ] && echo "$cur" && break + cur="$(dirname "$cur")" + done +} + +_ciel_source_env() { + local workdir="$(_ciel_find_ciel_workdir)" + [ -d "$workdir/.env" ] && source "$workdir/.env" +} + +_ciel_list_packages() { + local workdir="$(_ciel_find_ciel_workdir)" + [ -d "$workdir/TREE" ] || return + local PGROUPS="$(find "$workdir/TREE/groups/" -maxdepth 1 -mindepth 1 -type f -printf 'groups/%f\n')" + COMPREPLY+=($(compgen -W "$PGROUPS" -- "${1}")) + if [[ "$1" == *'/'* ]]; then + return + fi + COMPREPLY+=($(find "$workdir/TREE" -maxdepth 2 -mindepth 2 -type d -not -path "TREE/.git" -name "${1}*" -printf '%f\n')) +} + +_ciel_list_plugins() { + local CIEL="$(readlink -f $(command -v ciel))" + [ -z "$CIEL" ] && return + local PLUGIN_DIR="$(dirname $CIEL)/../libexec/ciel-plugin" + find "$PLUGIN_DIR" -maxdepth 1 -mindepth 1 -type f -name 'ciel-*' -printf '%f\n' | cut -d'-' -f2- +} + _ciel() { - local i cur prev opts cmd + local i cur prev opts cmds COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -15,9 +52,6 @@ _ciel() { ciel,add) cmd="ciel__add" ;; - ciel,boot) - cmd="ciel__boot" - ;; ciel,build) cmd="ciel__build" ;; @@ -33,8 +67,8 @@ _ciel() { ciel,del) cmd="ciel__del" ;; - ciel,diagnose) - cmd="ciel__diagnose" + ciel,doctor) + cmd="ciel__doctor" ;; ciel,down) cmd="ciel__down" @@ -45,8 +79,8 @@ _ciel() { ciel,help) cmd="ciel__help" ;; - ciel,instconf) - cmd="ciel__instconf" + ciel,init) + cmd="ciel__init" ;; ciel,list) cmd="ciel__list" @@ -81,15 +115,15 @@ _ciel() { ciel,update-os) cmd="ciel__update__os" ;; + ciel,update-tree) + cmd="ciel__update__tree" + ;; ciel,version) cmd="ciel__version" ;; ciel__help,add) cmd="ciel__help__add" ;; - ciel__help,boot) - cmd="ciel__help__boot" - ;; ciel__help,build) cmd="ciel__help__build" ;; @@ -105,8 +139,8 @@ _ciel() { ciel__help,del) cmd="ciel__help__del" ;; - ciel__help,diagnose) - cmd="ciel__help__diagnose" + ciel__help,doctor) + cmd="ciel__help__doctor" ;; ciel__help,down) cmd="ciel__help__down" @@ -117,8 +151,8 @@ _ciel() { ciel__help,help) cmd="ciel__help__help" ;; - ciel__help,instconf) - cmd="ciel__help__instconf" + ciel__help,init) + cmd="ciel__help__init" ;; ciel__help,list) cmd="ciel__help__list" @@ -153,21 +187,42 @@ _ciel() { ciel__help,update-os) cmd="ciel__help__update__os" ;; + ciel__help,update-tree) + cmd="ciel__help__update__tree" + ;; ciel__help,version) cmd="ciel__help__version" ;; + ciel__help__repo,deinit) + cmd="ciel__help__repo__deinit" + ;; + ciel__help__repo,init) + cmd="ciel__help__repo__init" + ;; ciel__help__repo,refresh) cmd="ciel__help__repo__refresh" ;; + ciel__repo,deinit) + cmd="ciel__repo__deinit" + ;; ciel__repo,help) cmd="ciel__repo__help" ;; + ciel__repo,init) + cmd="ciel__repo__init" + ;; ciel__repo,refresh) cmd="ciel__repo__refresh" ;; + ciel__repo__help,deinit) + cmd="ciel__repo__help__deinit" + ;; ciel__repo__help,help) cmd="ciel__repo__help__help" ;; + ciel__repo__help,init) + cmd="ciel__repo__help__init" + ;; ciel__repo__help,refresh) cmd="ciel__repo__help__refresh" ;; @@ -178,7 +233,7 @@ _ciel() { case "${cmd}" in ciel) - opts="-C -q -h -V --quiet --help --version version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" + opts="-C -b -h -V --batch --help --version version init load-os update-os load-tree update-tree new list add del shell run config commit doctor build rollback down stop mount farewell repo clean help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -196,57 +251,7 @@ _ciel() { return 0 ;; ciel__add) - opts="-h --local-repo --tmpfs --tmpfs-size --unset-tmpfs-size --ro-tree --output --unset-output --add-repo --remove-repo --unset-repo --add-nspawn-opt --remove-nspawn-opt --unset-nspawn-opt --help " - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --local-repo) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs-size) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --ro-tree) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --output) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - ciel__boot) - opts="-a -h --all --help [INSTANCE]..." + opts="-h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -260,57 +265,26 @@ _ciel() { return 0 ;; ciel__build) - opts="-i -g -c -h --local-repo --tmpfs --tmpfs-size --unset-tmpfs-size --ro-tree --output --unset-output --add-repo --remove-repo --unset-repo --add-nspawn-opt --remove-nspawn-opt --unset-nspawn-opt --resume --stage-select --always-discard --help [PACKAGES]..." - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + opts="-g -x -i -2 -c -h --offline --stage2 --resume --stage-select --help" + _ciel_source_env 2>/dev/null || true + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 && -z "${CIEL_INST}" ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in -i) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --local-repo) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs-size) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --ro-tree) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --output) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) return 0 ;; - --remove-nspawn-opt) + --resume) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --resume) + -c) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - -c) + --stage-select) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -319,6 +293,7 @@ _ciel() { ;; esac COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + _ciel_list_packages "$cur" return 0 ;; ciel__clean) @@ -336,12 +311,16 @@ _ciel() { return 0 ;; ciel__commit) - opts="-h --help " + opts="-i -h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + -i) + COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) + return 0 + ;; *) COMPREPLY=() ;; @@ -350,58 +329,14 @@ _ciel() { return 0 ;; ciel__config) - opts="-m -h --force-no-rollback --maintainer --dnssec --local-repo --source-cache --branch-exclusive-output --volatile-mount --use-apt --add-repo --remove-repo --unset-repo --add-nspawn-opt --remove-nspawn-opt --unset-nspawn-opt --help" + opts="-i -g -h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in - --maintainer) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -m) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --dnssec) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --local-repo) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --source-cache) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --branch-exclusive-output) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --volatile-mount) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --use-apt) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --add-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) + -i) + COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) return 0 ;; *) @@ -412,20 +347,19 @@ _ciel() { return 0 ;; ciel__del) - opts="-a -h --all --help [INSTANCE]..." - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + opts="-h --help" + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + if [[ ${cur} == -* ]] ; then return 0 fi case "${prev}" in *) - COMPREPLY=() ;; esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + COMPREPLY+=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) return 0 ;; - ciel__diagnose) + ciel__doctor) opts="-h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -440,12 +374,16 @@ _ciel() { return 0 ;; ciel__down) - opts="-a -h --all --help [INSTANCE]..." + opts="-i -h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + -i) + COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) + return 0 + ;; *) COMPREPLY=() ;; @@ -454,7 +392,7 @@ _ciel() { return 0 ;; ciel__farewell) - opts="-f -h --help" + opts="-h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -468,7 +406,7 @@ _ciel() { return 0 ;; ciel__help) - opts="version list new farewell load-os update-os instconf config load-tree add del mount boot stop down rollback commit shell run build repo clean diagnose help" + opts="version init load-os update-os load-tree update-tree new list add del shell run config commit doctor build rollback down stop mount farewell repo clean help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -495,20 +433,6 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__help__boot) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; ciel__help__build) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -579,7 +503,7 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__help__diagnose) + ciel__help__doctor) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -635,7 +559,7 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__help__instconf) + ciel__help__init) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -720,7 +644,7 @@ _ciel() { return 0 ;; ciel__help__repo) - opts="refresh" + opts="refresh init deinit" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -733,6 +657,34 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + ciel__help__repo__deinit) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ciel__help__repo__init) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; ciel__help__repo__refresh) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then @@ -817,6 +769,20 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + ciel__help__update__tree) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; ciel__help__version) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -831,53 +797,13 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__instconf) - opts="-i -h --force-no-rollback --local-repo --tmpfs --tmpfs-size --unset-tmpfs-size --ro-tree --output --unset-output --add-repo --remove-repo --unset-repo --add-nspawn-opt --remove-nspawn-opt --unset-nspawn-opt --help" + ciel__init) + opts="-h --upgrade --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in - -i) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --local-repo) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs-size) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --ro-tree) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --output) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; *) COMPREPLY=() ;; @@ -900,24 +826,12 @@ _ciel() { return 0 ;; ciel__load__os) - opts="-a -f -h --sha256 --arch --force --help [URL]" + opts="-h --help [url]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in - --sha256) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --arch) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -a) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; *) COMPREPLY=() ;; @@ -926,7 +840,7 @@ _ciel() { return 0 ;; ciel__load__tree) - opts="-h --help [URL]" + opts="-h --help [url]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -940,12 +854,16 @@ _ciel() { return 0 ;; ciel__mount) - opts="-a -h --all --help [INSTANCE]..." + opts="-i -h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + -i) + COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) + return 0 + ;; *) COMPREPLY=() ;; @@ -954,77 +872,13 @@ _ciel() { return 0 ;; ciel__new) - opts="-a -m -h --no-load-os --rootfs --sha256 --arch --no-load-tree --tree --maintainer --dnssec --local-repo --source-cache --branch-exclusive-output --volatile-mount --use-apt --add-repo --remove-repo --unset-repo --add-nspawn-opt --remove-nspawn-opt --unset-nspawn-opt --help" + opts="-h --from-tarball --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in - --rootfs) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --sha256) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --arch) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -a) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --tree) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --maintainer) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -m) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --dnssec) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --local-repo) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --source-cache) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --branch-exclusive-output) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --volatile-mount) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --use-apt) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --add-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-nspawn-opt) + --from-tarball) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -1036,7 +890,7 @@ _ciel() { return 0 ;; ciel__repo) - opts="-h --help refresh help" + opts="-h --help refresh init deinit help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1049,8 +903,22 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + ciel__repo__deinit) + opts="-h --help" + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + if [[ ${cur} == -* ]] ; then + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY+=( $(compgen -W "$(_ciel_list_instances)" -- "$cur") ) + return 0 + ;; ciel__repo__help) - opts="refresh help" + opts="refresh init deinit help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1063,6 +931,20 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + ciel__repo__help__deinit) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; ciel__repo__help__help) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then @@ -1077,6 +959,20 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + ciel__repo__help__init) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; ciel__repo__help__refresh) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then @@ -1091,8 +987,22 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + ciel__repo__init) + opts="-h --help" + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + if [[ ${cur} == -* ]] ; then + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY+=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) + return 0 + ;; ciel__repo__refresh) - opts="-h --help [PATH]" + opts="-h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1106,12 +1016,16 @@ _ciel() { return 0 ;; ciel__rollback) - opts="-a -h --all --help [INSTANCE]..." + opts="-i -h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + -i) + COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1127,7 +1041,7 @@ _ciel() { fi case "${prev}" in -i) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) return 0 ;; *) @@ -1138,50 +1052,14 @@ _ciel() { return 0 ;; ciel__shell) - opts="-i -h --local-repo --tmpfs --tmpfs-size --unset-tmpfs-size --ro-tree --output --unset-output --add-repo --remove-repo --unset-repo --add-nspawn-opt --remove-nspawn-opt --unset-nspawn-opt --help [COMMANDS]..." + opts="-i -h --help [COMMANDS]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in -i) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --local-repo) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs-size) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --ro-tree) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --output) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-nspawn-opt) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) return 0 ;; *) @@ -1192,12 +1070,16 @@ _ciel() { return 0 ;; ciel__stop) - opts="-a -h --all --help [INSTANCE]..." + opts="-i -h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in + -i) + COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1206,45 +1088,31 @@ _ciel() { return 0 ;; ciel__update__os) - opts="-h --force-use-apt --local-repo --tmpfs --tmpfs-size --unset-tmpfs-size --ro-tree --output --unset-output --add-repo --remove-repo --unset-repo --add-nspawn-opt --remove-nspawn-opt --unset-nspawn-opt --help" + opts="-h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in - --local-repo) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --tmpfs-size) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --ro-tree) - COMPREPLY=($(compgen -W "true false" -- "${cur}")) - return 0 - ;; - --output) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --add-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --remove-repo) - COMPREPLY=($(compgen -f "${cur}")) - return 0 + *) + COMPREPLY=() ;; - --add-nspawn-opt) + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ciel__update__tree) + opts="-r -h --rebase --help [branch]" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --rebase) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --remove-nspawn-opt) + -r) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -1272,8 +1140,4 @@ _ciel() { esac } -if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then - complete -F _ciel -o nosort -o bashdefault -o default ciel -else - complete -F _ciel -o bashdefault -o default ciel -fi +complete -F _ciel -o bashdefault -o default ciel diff --git a/completions/ciel.fish b/completions/ciel.fish new file mode 100644 index 0000000..56a9a84 --- /dev/null +++ b/completions/ciel.fish @@ -0,0 +1,149 @@ +function __ciel_find_ciel_workdir + set cur (readlink -f $PWD) + while test "$cur" != "/" + if test -d "$cur/.ciel" + echo "$cur" + break + end + set cur (dirname $cur) + end +end + +function __ciel_list_instances + set workdir (__ciel_find_ciel_workdir) + if ! test -d "$workdir/".ciel/container/instances + return + end + find "$workdir/".ciel/container/instances -maxdepth 1 -mindepth 1 -type d -printf '%f\tInstance\n' +end + +function __ciel_list_packages + set workdir (__ciel_find_ciel_workdir) + if ! test -d "$workdir/"TREE + return + end + find "$workdir/TREE/groups/" -maxdepth 1 -mindepth 1 -type f -printf 'groups/%f\n' + if string match -q -- "*/*" "$current" + return + end + find "$workdir/TREE" -maxdepth 2 -mindepth 2 -type d -not -path "TREE/.git" -printf '%f\n' +end + +function __ciel_list_plugins + set ciel_path (readlink -f (command -v ciel)) + set ciel_plugin_dir (dirname $ciel_path)"/../libexec/ciel-plugin" + find "$ciel_plugin_dir" -maxdepth 1 -mindepth 1 -type f -printf '%f\t-Ciel plugin-\n' | cut -d'-' -f2- +end + +complete -c ciel -n "__fish_use_subcommand" -s C -d 'Set the CIEL! working directory' -r +complete -c ciel -n "__fish_use_subcommand" -s b -l batch -d 'Batch mode, no input required' +complete -c ciel -n "__fish_use_subcommand" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_use_subcommand" -s V -l version -d 'Print version information' +complete -c ciel -n "__fish_use_subcommand" -f -a "version" -d 'Display the version of CIEL!' +complete -c ciel -n "__fish_use_subcommand" -f -a "init" -d 'Initialize the work directory' +complete -c ciel -n "__fish_use_subcommand" -f -a "update-os" -d 'Update the OS in the container' +complete -c ciel -n "__fish_use_subcommand" -f -a "load-tree" -d 'Clone package tree from the link provided or AOSC OS ABBS main repository' +complete -c ciel -n "__fish_use_subcommand" -f -a "update-tree" -d 'Update the existing ABBS tree (fetch only) and optionally switch to a different branch' +complete -c ciel -n "__fish_use_subcommand" -f -a "new" -d 'Create a new CIEL workspace' +complete -c ciel -n "__fish_use_subcommand" -f -a "list" -d 'List all the instances under the specified working directory' +complete -c ciel -n "__fish_use_subcommand" -f -a "add" -d 'Add a new instance' +complete -c ciel -n "__fish_use_subcommand" -f -a "del" -d 'Remove an instance' +complete -c ciel -n "__fish_use_subcommand" -f -a "shell" -d 'Start an interactive shell' +complete -c ciel -n "__fish_use_subcommand" -f -a "run" -d 'Lower-level version of \'shell\', without login environment, without sourcing ~/.bash_profile' +complete -c ciel -n "__fish_use_subcommand" -f -a "config" -d 'Configure system and toolchain for building interactively' +complete -c ciel -n "__fish_use_subcommand" -f -a "commit" -d 'Commit changes onto the shared underlying OS' +complete -c ciel -n "__fish_use_subcommand" -f -a "doctor" -d 'Diagnose problems (hopefully)' +complete -c ciel -n "__fish_use_subcommand" -f -a "build" -d 'Build the packages using the specified instance' +complete -c ciel -n "__fish_use_subcommand" -f -a "rollback" -d 'Rollback all or specified instance' +complete -c ciel -n "__fish_use_subcommand" -f -a "down" -d 'Shutdown and unmount all or one instance' +complete -c ciel -n "__fish_use_subcommand" -f -a "stop" -d 'Shuts down an instance' +complete -c ciel -n "__fish_use_subcommand" -f -a "mount" -d 'Mount all or specified instance' +complete -c ciel -n "__fish_use_subcommand" -f -a "farewell" -d 'Remove everything related to CIEL!' +complete -c ciel -n "__fish_use_subcommand" -f -a "repo" -d 'Local repository operations' +complete -c ciel -n "__fish_use_subcommand" -f -a "clean" -d 'Clean all the output directories and source cache directories' +complete -c ciel -n "__fish_use_subcommand" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c ciel -n "__fish_seen_subcommand_from version" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from init" -l upgrade -d 'Upgrade Ciel workspace from an older version' +complete -c ciel -n "__fish_seen_subcommand_from init" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from load-os" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from update-os" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from load-tree" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from update-tree" -s r -l rebase -d 'Rebase the specified branch from the updated upstream' -r +complete -c ciel -n "__fish_seen_subcommand_from update-tree" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from new" -l from-tarball -d 'Create a new workspace from the specified tarball' -r +complete -c ciel -n "__fish_seen_subcommand_from new" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from list" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from add" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from del" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from shell" -s i -d 'Instance to be used' -r +complete -c ciel -n "__fish_seen_subcommand_from shell" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from run" -s i -d 'Instance to run command in' -r +complete -c ciel -n "__fish_seen_subcommand_from run" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from config" -s g -d 'Configure base system instead of an instance' +complete -c ciel -n "__fish_seen_subcommand_from config" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from commit" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from doctor" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from build" -s c -l resume -d 'Continue from a Ciel checkpoint' -r +complete -c ciel -n "__fish_seen_subcommand_from build" -l stage-select -d 'Select the starting point for a build' -r +complete -c ciel -n "__fish_seen_subcommand_from build" -s g -d 'Fetch source packages only' +complete -c ciel -n "__fish_seen_subcommand_from build" -s x -l offline -d 'Disable network in the container during the build' +complete -c ciel -n "__fish_seen_subcommand_from build" -s 2 -l stage2 -d 'Use stage 2 mode instead of the regular build mode' +complete -c ciel -n "__fish_seen_subcommand_from build" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from rollback" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from down" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from stop" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from mount" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from farewell" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit; and not __fish_seen_subcommand_from help" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit; and not __fish_seen_subcommand_from help" -f -a "refresh" -d 'Refresh the repository' +complete -c ciel -n "__fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit; and not __fish_seen_subcommand_from help" -f -a "init" -d 'Initialize the repository' +complete -c ciel -n "__fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit; and not __fish_seen_subcommand_from help" -f -a "deinit" -d 'Uninitialize the repository' +complete -c ciel -n "__fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c ciel -n "__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from refresh" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from init" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from deinit" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit; and not __fish_seen_subcommand_from help" -f -a "refresh" -d 'Refresh the repository' +complete -c ciel -n "__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit; and not __fish_seen_subcommand_from help" -f -a "init" -d 'Initialize the repository' +complete -c ciel -n "__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit; and not __fish_seen_subcommand_from help" -f -a "deinit" -d 'Uninitialize the repository' +complete -c ciel -n "__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c ciel -n "__fish_seen_subcommand_from clean" -s h -l help -d 'Print help information' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "version" -d 'Display the version of CIEL!' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "init" -d 'Initialize the work directory' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "load-os" -d 'Unpack OS tarball or fetch the latest BuildKit from the repository' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "update-os" -d 'Update the OS in the container' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "load-tree" -d 'Clone package tree from the link provided or AOSC OS ABBS main repository' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "update-tree" -d 'Update the existing ABBS tree (fetch only) and optionally switch to a different branch' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "new" -d 'Create a new CIEL workspace' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "list" -d 'List all the instances under the specified working directory' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "add" -d 'Add a new instance' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "del" -d 'Remove an instance' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "shell" -d 'Start an interactive shell' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "run" -d 'Lower-level version of \'shell\', without login environment, without sourcing ~/.bash_profile' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "config" -d 'Configure system and toolchain for building interactively' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "commit" -d 'Commit changes onto the shared underlying OS' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "doctor" -d 'Diagnose problems (hopefully)' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "build" -d 'Build the packages using the specified instance' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "rollback" -d 'Rollback all or specified instance' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "down" -d 'Shutdown and unmount all or one instance' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "stop" -d 'Shuts down an instance' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "mount" -d 'Mount all or specified instance' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "farewell" -d 'Remove everything related to CIEL!' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "repo" -d 'Local repository operations' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "clean" -d 'Clean all the output directories and source cache directories' +complete -c ciel -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from version; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from load-os; and not __fish_seen_subcommand_from update-os; and not __fish_seen_subcommand_from load-tree; and not __fish_seen_subcommand_from update-tree; and not __fish_seen_subcommand_from new; and not __fish_seen_subcommand_from list; and not __fish_seen_subcommand_from add; and not __fish_seen_subcommand_from del; and not __fish_seen_subcommand_from shell; and not __fish_seen_subcommand_from run; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from commit; and not __fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from build; and not __fish_seen_subcommand_from rollback; and not __fish_seen_subcommand_from down; and not __fish_seen_subcommand_from stop; and not __fish_seen_subcommand_from mount; and not __fish_seen_subcommand_from farewell; and not __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c ciel -n "__fish_seen_subcommand_from help; and __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit" -f -a "refresh" -d 'Refresh the repository' +complete -c ciel -n "__fish_seen_subcommand_from help; and __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit" -f -a "init" -d 'Initialize the repository' +complete -c ciel -n "__fish_seen_subcommand_from help; and __fish_seen_subcommand_from repo; and not __fish_seen_subcommand_from refresh; and not __fish_seen_subcommand_from init; and not __fish_seen_subcommand_from deinit" -f -a "deinit" -d 'Uninitialize the repository' +# Enhanced completions +complete -xc ciel -n "__fish_seen_subcommand_from build" -a "(__ciel_list_packages)" +complete -xc ciel -n "__fish_seen_subcommand_from build" -s i -d 'Instance to build in' -a "(__ciel_list_instances)" +complete -xc ciel -n "__fish_seen_subcommand_from run" -s i -d 'Instance to run command in' -a "(__ciel_list_instances)" +complete -xc ciel -n "__fish_seen_subcommand_from config" -s i -d 'Instance to be configured' -a "(__ciel_list_instances)" +complete -xc ciel -n "__fish_seen_subcommand_from commit" -s i -d 'Instance to be committed' -a "(__ciel_list_instances)" +complete -xc ciel -n "__fish_seen_subcommand_from build" -s i -d 'Instance to build in' -a "(__ciel_list_instances)" +complete -xc ciel -n "__fish_seen_subcommand_from rollback" -s i -d 'Instance to be rolled back' -a "(__ciel_list_instances)" +complete -xc ciel -n "__fish_seen_subcommand_from down" -s i -d 'Instance to be un-mounted' -a "(__ciel_list_instances)" +complete -xc ciel -n "__fish_seen_subcommand_from stop" -s i -d 'Instance to be stopped' -a "(__ciel_list_instances)" +complete -xc ciel -n "__fish_seen_subcommand_from mount" -s i -d 'Instance to be mounted' -a "(__ciel_list_instances)" +complete -xc ciel -n "__fish_seen_subcommand_from load-os" -a "(__fish_complete_suffix tar.xz)" +complete -c ciel -n "__fish_use_subcommand" -f -a "(__ciel_list_plugins)" diff --git a/dbus-xml/org.freedesktop.machine1-machine.xml b/dbus-xml/org.freedesktop.machine1-machine.xml index 5a1e97b..4c10aee 100644 --- a/dbus-xml/org.freedesktop.machine1-machine.xml +++ b/dbus-xml/org.freedesktop.machine1-machine.xml @@ -1,5 +1,5 @@ +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> @@ -9,26 +9,26 @@ - + - - + + - - + + - - + + - + @@ -64,15 +64,6 @@ - - - - - - - - - @@ -85,10 +76,6 @@ - - - - @@ -125,16 +112,6 @@ - - - - - - - - - - diff --git a/dbus-xml/org.freedesktop.machine1-manager.xml b/dbus-xml/org.freedesktop.machine1.xml similarity index 88% rename from dbus-xml/org.freedesktop.machine1-manager.xml rename to dbus-xml/org.freedesktop.machine1.xml index 760f3e9..80425a0 100644 --- a/dbus-xml/org.freedesktop.machine1-manager.xml +++ b/dbus-xml/org.freedesktop.machine1.xml @@ -1,5 +1,5 @@ +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> @@ -9,26 +9,26 @@ - + - - + + - - + + - - + + - + @@ -120,11 +120,6 @@ - - - - - @@ -166,18 +161,6 @@ - - - - - - - - - - - - diff --git a/dbus-xml/org.freedesktop.systemd1.xml b/dbus-xml/org.freedesktop.systemd1.xml new file mode 100644 index 0000000..c9be35e --- /dev/null +++ b/dbus-xml/org.freedesktop.systemd1.xml @@ -0,0 +1,715 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/install-assets.sh b/install-assets.sh index a7a070e..55434c0 100755 --- a/install-assets.sh +++ b/install-assets.sh @@ -8,8 +8,8 @@ install -Dvm755 plugins/* "${PREFIX}/libexec/ciel-plugin" # install completions install -dv "${PREFIX}/share/zsh/functions/Completion/Linux/" -install -Dvm644 cli/completions/_ciel "${PREFIX}/share/zsh/functions/Completion/Linux/" +install -Dvm644 completions/_ciel "${PREFIX}/share/zsh/functions/Completion/Linux/" install -dv "${PREFIX}/share/fish/vendor_completions.d/" -install -Dvm644 cli/completions/ciel.fish "${PREFIX}/share/fish/vendor_completions.d/" +install -Dvm644 completions/ciel.fish "${PREFIX}/share/fish/vendor_completions.d/" install -dv "${PREFIX}/share/bash-completion/completions/" -install -Dvm644 cli/completions/ciel.bash "${PREFIX}/share/bash-completion/completions/" +install -Dvm644 completions/ciel.bash "${PREFIX}/share/bash-completion/completions/" diff --git a/src/actions/container.rs b/src/actions/container.rs new file mode 100644 index 0000000..cfdf4d2 --- /dev/null +++ b/src/actions/container.rs @@ -0,0 +1,406 @@ +use anyhow::{anyhow, Result}; +use console::{style, user_attended}; +use dialoguer::{theme::ColorfulTheme, Confirm, Input}; +use git2::Repository; +use nix::unistd::sync; +use rand::random; +use std::{ + ffi::OsStr, + fs, + path::{Path, PathBuf}, +}; + +use crate::{ + actions::{ensure_host_sanity, OMA_UPDATE_SCRIPT}, + common::*, + config, error, info, + machine::{self, get_container_ns_name, inspect_instance, spawn_container}, + network::download_file_progress, + overlayfs, warn, +}; + +use super::{for_each_instance, APT_UPDATE_SCRIPT}; + +/// Get the branch name of the workspace TREE repository +#[inline] +pub fn get_branch_name() -> Result { + let repo = Repository::open("TREE")?; + let head = repo.head()?; + + Ok(head + .shorthand() + .ok_or_else(|| anyhow!("Unable to resolve Git ref"))? + .to_owned()) +} + +/// Determine the output directory name +#[inline] +pub fn get_output_directory(sep_mount: bool) -> String { + if sep_mount { + format!( + "OUTPUT-{}", + get_branch_name().unwrap_or_else(|_| "HEAD".to_string()) + ) + } else { + "OUTPUT".to_string() + } +} + +fn commit(instance: &str) -> Result<()> { + get_instance_ns_name(instance)?; + info!("Un-mounting all the instances..."); + // Un-mount all the instances + for_each_instance(&container_down)?; + info!("{}: committing instance...", instance); + let spinner = create_spinner("Committing upper layer...", 200); + let man = &mut *overlayfs::get_overlayfs_manager(instance)?; + man.commit()?; + sync(); + spinner.finish_and_clear(); + + Ok(()) +} + +/// Rollback the container (by removing the upper layer) +fn rollback(instance: &str) -> Result<()> { + get_instance_ns_name(instance)?; + info!("{}: rolling back instance...", instance); + let spinner = create_spinner("Removing upper layer...", 200); + let man = &mut *overlayfs::get_overlayfs_manager(instance)?; + man.rollback()?; + sync(); + spinner.finish_and_clear(); + + Ok(()) +} + +/// Remove everything in the current workspace +pub fn farewell(path: &Path) -> Result<()> { + if !user_attended() { + eprintln!("DELETE THIS CIEL WORKSPACE?"); + info!("Not controlled by an user. Automatically confirmed."); + // Un-mount all the instances + for_each_instance(&container_down)?; + fs::remove_dir_all(path.join(".ciel"))?; + return Ok(()); + } + let theme = ColorfulTheme::default(); + let delete = Confirm::with_theme(&theme) + .with_prompt("DELETE THIS CIEL WORKSPACE?") + .default(false) + .interact()?; + if !delete { + info!("Not confirmed."); + return Ok(()); + } + info!( + "If you are absolutely sure, please type the following:\n{}", + style("Do as I say!").bold() + ); + if Input::::with_theme(&theme) + .with_prompt("Your turn") + .interact()? + != "Do as I say!" + { + info!("Prompt answered incorrectly. Not confirmed."); + return Ok(()); + } + + info!("... as you wish. Commencing destruction ..."); + info!("Un-mounting all the instances..."); + // Un-mount all the instances + for_each_instance(&container_down)?; + fs::remove_dir_all(path.join(".ciel"))?; + + Ok(()) +} + +/// Download the OS tarball and then extract it for use as the base layer +pub fn load_os(url: &str, sha256: Option, tarball: bool) -> Result<()> { + info!("Downloading base OS rootfs..."); + let path = Path::new(url); + let filename = path + .file_name() + .ok_or_else(|| anyhow!("Unable to convert path to string"))? + .to_str() + .ok_or_else(|| anyhow!("Unable to decode path string"))?; + let is_local_file = path.is_file(); + let total = if !is_local_file { + download_file_progress(url, filename)? + } else { + let tarball = fs::File::open(path)?; + tarball.metadata()?.len() + }; + if let Some(sha256) = sha256 { + info!("Verifying tarball checksum..."); + let tarball = fs::File::open(Path::new(filename))?; + let checksum = sha256sum(tarball)?; + if sha256 == checksum { + info!("Checksum verified."); + } else { + return Err(anyhow!( + "Checksum mismatch: expected {} but got {}", + sha256, + checksum + )); + } + } + + if is_local_file { + extract_system_rootfs(&PathBuf::from(path), total, tarball)?; + } else { + extract_system_rootfs(Path::new(filename), total, tarball)?; + } + + Ok(()) +} + +/// Ask user for the configuration and then apply it +pub fn config_os(instance: Option<&str>) -> Result<()> { + let config; + let mut prev_volatile = None; + if let Ok(c) = config::read_config() { + prev_volatile = Some(c.volatile_mount); + config = config::ask_for_config(Some(c)); + } else { + config = config::ask_for_config(None); + } + let path; + if let Some(instance) = instance { + let man = &mut *overlayfs::get_overlayfs_manager(instance)?; + path = man.get_config_layer()?; + } else { + path = PathBuf::from(CIEL_DIST_DIR); + } + if let Ok(c) = config { + info!("Shutting down instance(s) before applying config..."); + if let Some(instance) = instance { + container_down(instance)?; + } else { + for_each_instance(&container_down)?; + } + config::apply_config(path, &c)?; + fs::create_dir_all(CIEL_DATA_DIR)?; + fs::write( + Path::new(CIEL_DATA_DIR).join("config.toml"), + c.save_config()?, + )?; + info!("Configurations applied."); + let volatile_changed = if let Some(prev_voltile) = prev_volatile { + prev_voltile != c.volatile_mount + } else { + false + }; + if volatile_changed { + warn!("You have changed the volatile mount option, please save your work and\x1b[1m\x1b[93m rollback \x1b[4mall the instances\x1b[0m."); + return Ok(()); + } + warn!( + "Please rollback {} for the new config to take effect!", + instance.unwrap_or("all your instances"), + ); + } else { + return Err(anyhow!("Could not recognize the configuration.")); + } + + Ok(()) +} + +/// Mount the filesystem of the instance +pub fn mount_fs(instance: &str) -> Result<()> { + let config = config::read_config()?; + let man = &mut *overlayfs::get_overlayfs_manager(instance)?; + man.set_volatile(config.volatile_mount)?; + machine::mount_layers(man, instance)?; + info!("{}: filesystem mounted.", instance); + + Ok(()) +} + +/// Un-mount the filesystem of the container +pub fn unmount_fs(instance: &str) -> Result<()> { + let man = &mut *overlayfs::get_overlayfs_manager(instance)?; + let target = std::env::current_dir()?.join(instance); + let mut retry = 0usize; + while man.is_mounted(&target)? { + retry += 1; + if retry > 10 { + return Err(anyhow!("Unable to unmount filesystem after 10 attempts.")); + } + man.unmount(&target)?; + } + info!("{}: filesystem un-mounted.", instance); + + Ok(()) +} + +/// Remove the mount point (usually a directory) of the container overlay filesystem +pub fn remove_mount(instance: &str) -> Result<()> { + let target = std::env::current_dir()?.join(instance); + if !target.exists() { + return Ok(()); + } else if !target.is_dir() { + warn!("{}: mount point is not a directory.", instance); + return Ok(()); + } + match fs::read_dir(&target) { + Ok(mut entry) => { + if entry.any(|_| true) { + warn!( + "Mount point {:?} still contains files, so it will not be removed.", + target + ); + return Ok(()); + } + } + Err(e) => { + error!("Error when querying {:?}: {}", target, e); + } + } + fs::remove_dir(target)?; + info!("{}: mount point removed.", instance); + + Ok(()) +} + +fn get_instance_ns_name(instance: &str) -> Result { + if !is_instance_exists(instance) { + error!("Instance `{}` does not exist.", instance); + info!( + "You can add a new instance like this: `ciel add {}`", + instance + ); + return Err(anyhow!("Unable to acquire container information.")); + } + let legacy = is_legacy_workspace()?; + + get_container_ns_name(instance, legacy) +} + +/// Start the container/instance, also mounting the container filesystem prior to the action +pub fn start_container(instance: &str) -> Result { + let ns_name = get_instance_ns_name(instance)?; + let inst = inspect_instance(instance, &ns_name)?; + let (mut extra_options, mounts) = ensure_host_sanity()?; + if std::env::var("CIEL_OFFLINE").is_ok() { + // FIXME: does not work with current version of systemd + // add the offline option (private-network means don't share the host network) + extra_options.push("--private-network".to_string()); + info!("{}: network disconnected.", instance); + } + if !inst.mounted { + mount_fs(instance)?; + } + if !inst.started { + spawn_container(&ns_name, instance, &extra_options, &mounts)?; + } + + Ok(ns_name) +} + +/// Execute the specified command in the container +pub fn run_in_container>(instance: &str, args: &[S]) -> Result { + let ns_name = start_container(instance)?; + let status = machine::execute_container_command(&ns_name, args)?; + + Ok(status) +} + +/// Stop the container/instance (without un-mounting the filesystem) +pub fn stop_container(instance: &str) -> Result<()> { + let ns_name = get_instance_ns_name(instance)?; + let inst = inspect_instance(instance, &ns_name)?; + if !inst.started { + info!("{}: instance is not running!", instance); + return Ok(()); + } + info!("{}: stopping...", instance); + machine::terminate_container_by_name(&ns_name)?; + machine::clean_child_process(); + info!("{}: instance stopped.", instance); + + Ok(()) +} + +/// Stop and un-mount the container and its filesystem +pub fn container_down(instance: &str) -> Result<()> { + stop_container(instance)?; + unmount_fs(instance)?; + remove_mount(instance)?; + + Ok(()) +} + +/// Commit the container/instance upper layer changes to the base layer of the filesystem +pub fn commit_container(instance: &str) -> Result<()> { + container_down(instance)?; + commit(instance)?; + info!("{}: instance has been committed.", instance); + + Ok(()) +} + +/// Clear the upper layer of the container/instance filesystem +pub fn rollback_container(instance: &str) -> Result<()> { + container_down(instance)?; + rollback(instance)?; + info!("{}: instance has been rolled back.", instance); + + Ok(()) +} + +/// Create a new instance +#[inline] +pub fn add_instance(instance: &str) -> Result<()> { + overlayfs::create_new_instance_fs(CIEL_INST_DIR, instance)?; + info!("{}: instance created.", instance); + + Ok(()) +} + +/// Remove the container/instance and its filesystem from the host filesystem +pub fn remove_instance(instance: &str) -> Result<()> { + container_down(instance)?; + info!("{}: removing instance...", instance); + let spinner = create_spinner("Removing the instance...", 200); + let man = &mut *overlayfs::get_overlayfs_manager(instance)?; + man.destroy()?; + spinner.finish_and_clear(); + info!("{}: instance removed.", instance); + + Ok(()) +} + +/// Update AOSC OS in the container/instance +pub fn update_os(force_use_apt: bool) -> Result<()> { + info!("Updating base OS..."); + let instance = format!("update-{:x}", random::()); + add_instance(&instance)?; + + if force_use_apt { + return apt_update_os(&instance); + } + + let status = run_in_container(&instance, &["/bin/bash", "-ec", OMA_UPDATE_SCRIPT])?; + if status != 0 { + return apt_update_os(&instance); + } + + commit_container(&instance)?; + remove_instance(&instance)?; + + Ok(()) +} + +fn apt_update_os(instance: &str) -> Result<()> { + let status = run_in_container(instance, &["/bin/bash", "-ec", APT_UPDATE_SCRIPT])?; + + if status != 0 { + return Err(anyhow!("Failed to update OS: {}", status)); + } + + commit_container(instance)?; + remove_instance(instance)?; + + Ok(()) +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs new file mode 100644 index 0000000..ef52416 --- /dev/null +++ b/src/actions/mod.rs @@ -0,0 +1,65 @@ +use anyhow::Result; +use console::style; + +use crate::machine; + +mod container; +mod onboarding; +mod packaging; + +// re-export all the functions from the sub +pub use self::container::*; +pub use self::onboarding::onboarding; +pub use self::packaging::*; + +const DEFAULT_MOUNTS: &[(&str, &str)] = &[ + ("OUTPUT/debs/", "/debs/"), + ("TREE", "/tree"), + ("SRCS", "/var/cache/acbs/tarballs"), + ("CACHE", "/var/cache/apt/archives"), +]; +const APT_UPDATE_SCRIPT: &str = r#"export DEBIAN_FRONTEND=noninteractive;apt-get update -y --allow-releaseinfo-change && apt-get -y -o Dpkg::Options::="--force-confnew" full-upgrade --autoremove --purge && apt autoclean"#; +const OMA_UPDATE_SCRIPT: &str = r#"oma upgrade -y --force-confnew --no-progress --force-unsafe-io && oma autoremove -y --remove-config && oma clean"#; + +type MountOptions = (Vec, Vec<(String, &'static str)>); +/// Ensure that the directories exist and mounted +pub fn ensure_host_sanity() -> Result { + use crate::warn; + + let mut extra_options = Vec::new(); + let mut mounts: Vec<(String, &str)> = DEFAULT_MOUNTS + .iter() + .map(|x| (x.0.to_string(), x.1)) + .collect(); + if let Ok(c) = crate::config::read_config() { + extra_options = c.extra_options; + if !c.local_sources { + // remove SRCS + mounts.swap_remove(2); + } + if c.sep_mount { + mounts.push((format!("{}/debs", get_output_directory(true)), "/debs/")); + mounts.swap_remove(0); + } + } else { + warn!("This workspace is not yet configured, default settings are used."); + } + + for mount in &mounts { + std::fs::create_dir_all(&mount.0)?; + } + + Ok((extra_options, mounts)) +} + +/// A convenience function for iterating over all the instances while executing the actions +#[inline] +pub fn for_each_instance Result<()>>(func: &F) -> Result<()> { + let instances = machine::list_instances_simple()?; + for instance in instances { + eprintln!("{} {}", style(">>>").bold(), style(&instance).cyan().bold()); + func(&instance)?; + } + + Ok(()) +} diff --git a/src/actions/onboarding.rs b/src/actions/onboarding.rs new file mode 100644 index 0000000..e0c6f62 --- /dev/null +++ b/src/actions/onboarding.rs @@ -0,0 +1,151 @@ +use anyhow::{anyhow, Result}; +use console::{style, user_attended, Term}; +use dialoguer::{theme::ColorfulTheme, Confirm, Input}; +use std::{fs, path::Path, process::exit}; + +use crate::{ + actions::get_branch_name, + cli::GIT_TREE_URL, + common::*, + config, error, info, + network::{download_git, pick_latest_rootfs}, + overlayfs::create_new_instance_fs, + repo::{init_repo, refresh_repo}, + warn, +}; + +use super::{load_os, mount_fs}; + +/// Show interactive onboarding guide, triggered by issuing `ciel new` +pub fn onboarding(custom_tarball: Option<&String>, arch: Option<&str>) -> Result<()> { + ctrlc::set_handler(move || { + let _ = Term::stderr().show_cursor(); + exit(1); + }) + .expect("Error setting Ctrl-C handler"); + + let theme = ColorfulTheme::default(); + info!("Welcome to ciel!"); + if Path::new(".ciel").exists() { + error!("Seems like you've already created a ciel workspace here."); + info!("Please run `ciel farewell` to nuke it before running this command."); + return Err(anyhow!("Unable to create a ciel workspace.")); + } + info!("Before continuing, I need to ask you a few questions:"); + let real_arch = if let Some(arch) = arch { + arch + } else if custom_tarball.is_some() { + "custom" + } else { + ask_for_target_arch()? + }; + let config = config::ask_for_config(None)?; + let mut init_instance: Option = None; + if user_attended() + && Confirm::with_theme(&theme) + .with_prompt("Do you want to add a new instance now?") + .interact()? + { + let name: String = Input::with_theme(&theme) + .with_prompt("Name of the instance") + .interact_text()?; + init_instance = Some(name.clone()); + info!( + "Understood. `{}` will be created after initialization is finished.", + name + ); + } else { + info!("Okay. You can always add a new instance later."); + } + + info!("Initializing workspace..."); + ciel_init()?; + info!("Initializing container OS..."); + let (rootfs_url, rootfs_sha256, use_tarball) = match custom_tarball { + Some(rootfs) => { + let use_tarball = !rootfs.ends_with(".squashfs"); + info!( + "Using custom {} from {}", + if use_tarball { "tarball" } else { "squashfs" }, + rootfs + ); + (rootfs.clone(), None, use_tarball) + } + None => { + info!("Searching for latest AOSC OS buildkit release..."); + auto_pick_rootfs(&theme, real_arch)? + } + }; + load_os(&rootfs_url, rootfs_sha256, use_tarball)?; + info!("Initializing ABBS tree..."); + if Path::new("TREE").is_dir() { + warn!("TREE already exists, skipping this step..."); + } else { + // if TREE is a file, then remove it + fs::remove_file("TREE").ok(); + download_git(GIT_TREE_URL, Path::new("TREE"))?; + } + config::apply_config(CIEL_DIST_DIR, &config)?; + info!("Applying configurations..."); + fs::write( + Path::new(CIEL_DATA_DIR).join("config.toml"), + config.save_config()?, + )?; + info!("Configurations applied."); + let cwd = std::env::current_dir()?; + let mut output_dir_name = "OUTPUT".to_string(); + + if config.sep_mount { + output_dir_name.push('-'); + output_dir_name.push_str(&get_branch_name()?); + } + + if config.local_repo { + info!("Setting up local repository ..."); + refresh_repo(&cwd.join(&output_dir_name))?; + info!("Local repository ready."); + } + + if let Some(init_instance) = init_instance { + create_new_instance_fs(CIEL_INST_DIR, &init_instance)?; + info!("{}: instance initialized.", init_instance); + if config.local_repo { + mount_fs(&init_instance)?; + init_repo(&cwd.join(output_dir_name), &cwd.join(&init_instance))?; + info!("{}: local repository initialized.", init_instance); + } + } + + Ok(()) +} + +#[inline] +fn auto_pick_rootfs( + theme: &dyn dialoguer::theme::Theme, + arch: &str, +) -> Result<(String, Option, bool)> { + let root = pick_latest_rootfs(arch); + + if let Ok(rootfs) = root { + info!( + "Ciel has picked buildkit for {}, released on {}", + rootfs.arch, rootfs.date + ); + Ok(( + format!("https://releases.aosc.io/{}", rootfs.path), + Some(rootfs.sha256sum), + false, + )) + } else { + warn!( + "Ciel was unable to find a suitable buildkit release. Please specify the URL manually." + ); + let rootfs_url = Input::::with_theme(theme) + .with_prompt("Rootfs URL") + .interact_text()?; + + let use_tarball = !rootfs_url.ends_with(".squashfs"); + + Ok((rootfs_url, None, use_tarball)) + } +} diff --git a/src/actions/packaging.rs b/src/actions/packaging.rs new file mode 100644 index 0000000..f05c762 --- /dev/null +++ b/src/actions/packaging.rs @@ -0,0 +1,378 @@ +use anyhow::{anyhow, Result}; +use console::style; +use dialoguer::{theme::ColorfulTheme, Select}; +use nix::unistd::gethostname; +use serde::{Deserialize, Serialize}; +use std::{ + fs::{self, File}, + io::{BufRead, BufReader, Write}, + path::Path, + thread::{self, sleep}, + time::{Duration, Instant}, +}; +use walkdir::WalkDir; + +use crate::{actions::OMA_UPDATE_SCRIPT, common::create_spinner, config, error, info, repo, warn}; + +use super::{ + container::{get_output_directory, mount_fs, rollback_container, run_in_container}, + APT_UPDATE_SCRIPT, +}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct BuildCheckPoint { + packages: Vec, + progress: usize, + time_elapsed: usize, + attempts: usize, +} + +#[derive(Debug, Copy, Clone)] +pub struct BuildSettings { + pub offline: bool, + pub stage2: bool, +} + +pub fn load_build_checkpoint>(path: P) -> Result { + let f = File::open(path)?; + + Ok(bincode::deserialize_from(f)?) +} + +fn dump_build_checkpoint(checkpoint: &BuildCheckPoint) -> Result<()> { + let save_state = bincode::serialize(checkpoint)?; + let last_package = checkpoint + .packages + .get(checkpoint.progress) + .map_or("unknown".to_string(), |x| x.to_owned()); + let last_package = last_package.replace('/', "_"); + let current = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + fs::create_dir_all("./STATES")?; + let path = Path::new("./STATES").join(format!("{}-{}.ciel-ckpt", last_package, current)); + let mut f = File::create(&path)?; + f.write_all(&save_state)?; + info!("Ciel created a check-point: {}", path.display()); + + Ok(()) +} + +#[inline] +fn format_duration(seconds: u64) -> String { + format!( + "{:02}:{:02}:{:02}", + seconds / 3600, + (seconds / 60) % 60, + seconds % 60 + ) +} + +fn read_package_list>(filename: P, depth: usize) -> Result> { + if depth > 32 { + return Err(anyhow!( + "Nested group exceeded 32 levels! Potential infinite loop." + )); + } + let f = fs::File::open(filename)?; + let reader = BufReader::new(f); + let mut results = Vec::new(); + for line in reader.lines() { + let line = line?; + // skip comment + if line.starts_with('#') { + continue; + } + // trim whitespace + let trimmed = line.trim(); + // skip empty line + if trimmed.is_empty() { + continue; + } + // process nested groups + if trimmed.starts_with("groups/") { + let path = Path::new("./TREE").join(trimmed); + let nested = read_package_list(&path, depth + 1)?; + results.extend(nested); + continue; + } + results.push(trimmed.to_owned()); + } + + Ok(results) +} + +/// Expand the packages list to an array of packages +fn expand_package_list, I: IntoIterator>(packages: I) -> Vec { + let mut expanded = Vec::new(); + for package in packages { + let package = package.as_ref(); + if !package.starts_with("groups/") { + expanded.push(package.to_string()); + continue; + } + let list_file = Path::new("./TREE").join(package); + match read_package_list(list_file, 0) { + Ok(list) => { + info!("Read {} packages from {}", list.len(), package); + expanded.extend(list); + } + Err(e) => { + warn!("Unable to read package group `{}`: {}", package, e); + } + } + } + + expanded +} + +struct RepoMonitorGuard { + _handle: thread::JoinHandle, + stop_sender: std::sync::mpsc::Sender<()>, +} + +impl RepoMonitorGuard { + fn new(handle: thread::JoinHandle, stop_sender: std::sync::mpsc::Sender<()>) -> Self { + Self { + _handle: handle, + stop_sender, + } + } +} + +impl Drop for RepoMonitorGuard { + fn drop(&mut self) { + self.stop_sender.send(()).ok(); + } +} + +#[inline] +fn package_build_inner>( + packages: &[String], + instance: &str, + root: P, +) -> Result<(i32, usize)> { + let total = packages.len(); + let hostname = gethostname().map_or_else( + |_| "unknown".to_string(), + |s| s.into_string().unwrap_or_else(|_| "unknown".to_string()), + ); + let (tx, rx) = std::sync::mpsc::channel(); + let root_path = root.as_ref().to_path_buf(); + let refresh_monitor = thread::spawn(move || repo::start_monitor(&root_path, rx)); + let guard = RepoMonitorGuard::new(refresh_monitor, tx); + for (index, package) in packages.iter().enumerate() { + // set terminal title, \r is for hiding the message if the terminal does not support the sequence + eprint!( + "\x1b]0;ciel: [{}/{}] {} ({}@{})\x07\r", + index + 1, + total, + package, + instance, + hostname + ); + // hopefully the sequence gets flushed together with the `info!` below + info!("[{}/{}] Building {}...", index + 1, total, package); + mount_fs(instance)?; + info!("Refreshing local repository..."); + repo::init_repo(root.as_ref(), Path::new(instance))?; + let mut status = -1; + let mut oma = true; + for i in 1..=5 { + status = if oma { + run_in_container(instance, &["/bin/bash", "-ec", OMA_UPDATE_SCRIPT]).unwrap_or(-1) + } else { + run_in_container(instance, &["/bin/bash", "-ec", APT_UPDATE_SCRIPT]).unwrap_or(-1) + }; + if status == 0 { + break; + } else { + let interval = 3u64.pow(i); + warn!( + "Failed to update the OS, will retry in {} seconds ...", + interval + ); + oma = false; + sleep(Duration::from_secs(interval)); + } + } + if status != 0 { + error!("Failed to update the OS before building packages"); + return Ok((status, index)); + } + let status = run_in_container(instance, &["/bin/acbs-build", "--", package])?; + if status != 0 { + error!("Build failed with status: {}", status); + return Ok((status, index)); + } + rollback_container(instance)?; + } + drop(guard); + + Ok((0, 0)) +} + +pub fn packages_stage_select, K: Clone + ExactSizeIterator>( + instance: &str, + packages: K, + settings: BuildSettings, + start_package: Option<&String>, +) -> Result { + let packages = expand_package_list(packages); + + let selection = if let Some(start_package) = start_package { + packages + .iter() + .position(|x| { + x == start_package || x.split_once('/').map(|x| x.1) == Some(start_package) + }) + .ok_or_else(|| anyhow!("Can not find the specified package in the list!"))? + } else { + eprintln!("-*-* S T A G E\t\tS E L E C T *-*-"); + + Select::with_theme(&ColorfulTheme::default()) + .default(0) + .with_prompt( + "Choose a package to start building from (left/right arrow keys to change pages)", + ) + .items(&packages) + .interact()? + }; + let empty: Vec<&str> = Vec::new(); + + package_build( + instance, + empty.into_iter(), + Some(BuildCheckPoint { + packages, + progress: selection, + time_elapsed: 0, + attempts: 1, + }), + settings, + ) +} + +/// Fetch all the source packages in one go +pub fn package_fetch>(instance: &str, packages: &[S]) -> Result { + let conf = config::read_config(); + if conf.is_err() { + return Err(anyhow!("Please configure this workspace first!")); + } + let conf = conf.unwrap(); + if !conf.local_sources { + warn!("Using this function without local sources caching is probably meaningless."); + } + + mount_fs(instance)?; + rollback_container(instance)?; + + let mut cmd = vec!["/bin/acbs-build", "-g", "--"]; + cmd.extend(packages.iter().map(|p| p.as_ref())); + let status = run_in_container(instance, &cmd)?; + + Ok(status) +} + +/// Build packages in the container +pub fn package_build, K: Clone + ExactSizeIterator>( + instance: &str, + packages: K, + state: Option, + settings: BuildSettings, +) -> Result { + let conf = config::read_config(); + if conf.is_err() { + return Err(anyhow!("Please configure this workspace first!")); + } + let conf = conf.unwrap(); + let mut attempts = 1usize; + + let packages = if let Some(p) = state { + attempts = p.attempts + 1; + info!( + "Successfully restored from a checkpoint. Attempt #{} started.", + attempts + ); + p.packages[p.progress..].to_owned() + } else { + expand_package_list(packages) + }; + + if settings.offline || std::env::var("CIEL_OFFLINE").is_ok() { + info!("Preparing offline mode. Fetching source packages first ..."); + package_fetch(instance, &packages)?; + std::env::set_var("CIEL_OFFLINE", "ON"); + // FIXME: does not work with current version of systemd + info!("Running in offline mode. Network access disabled."); + } + + if settings.stage2 { + std::env::set_var("CIEL_STAGE2", "ON"); + info!("Running in stage 2 mode. ACBS and autobuild3 may behave differently."); + } + + mount_fs(instance)?; + rollback_container(instance)?; + + if !conf.local_repo { + let mut cmd = vec!["/bin/acbs-build".to_string(), "--".to_string()]; + cmd.extend(packages); + let status = run_in_container(instance, &cmd)?; + return Ok(status); + } + + let output_dir = get_output_directory(conf.sep_mount); + let root = std::env::current_dir()?.join(output_dir); + let total = packages.len(); + let start = Instant::now(); + let (exit_status, progress) = package_build_inner(&packages, instance, root)?; + if exit_status != 0 { + let checkpoint = BuildCheckPoint { + packages, + progress, + attempts, + time_elapsed: 0, + }; + if std::env::var("CIEL_NO_CHECKPOINT").is_err() { + dump_build_checkpoint(&checkpoint)?; + } + return Ok(exit_status); + } + let duration = start.elapsed().as_secs(); + eprintln!( + "{} - {} packages in {}", + style("BUILD SUCCESSFUL").bold().green(), + total, + format_duration(duration) + ); + + Ok(0) +} + +/// Clean up output directories +pub fn cleanup_outputs() -> Result<()> { + let spinner = create_spinner("Removing output directories ...", 200); + for entry in WalkDir::new(".").max_depth(1) { + let entry = entry?; + if entry.file_type().is_dir() && entry.file_name().to_string_lossy().starts_with("OUTPUT-") + { + fs::remove_dir_all(entry.path())?; + } + } + if Path::new("./SRCS").is_dir() { + fs::remove_dir_all("./SRCS")?; + } + if Path::new("./STATES").is_dir() { + fs::remove_dir_all("./STATES")?; + } + spinner.finish_with_message("Done."); + + Ok(()) +} + +#[test] +fn test_time_format() { + let test_dur = 3661; + assert_eq!(format_duration(test_dur), "01:01:01"); +} diff --git a/src/build.rs b/src/build.rs deleted file mode 100644 index 6539ae9..0000000 --- a/src/build.rs +++ /dev/null @@ -1,300 +0,0 @@ -use std::{ - fs::{self}, - io::{BufRead, BufReader}, - path::Path, - process::ExitStatus, - time::{Duration, Instant}, -}; - -use log::{info, warn}; -use nix::unistd::gethostname; -use serde::{Deserialize, Serialize}; - -use crate::{ - repo::monitor::RepositoryRefreshMonitor, Container, Error, Result, SimpleAptRepository, - Workspace, -}; - -/// A build request. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct BuildRequest { - /// Packages to build. - /// - /// Package groups (`groups/xxx`) will be expanded on [BuildRequest::execute]. - pub packages: Vec, - /// Fetch-sources only mode. - pub fetch_only: bool, -} - -impl BuildRequest { - /// Creates a new build request. - pub fn new(packages: Vec) -> Self { - Self { - packages, - fetch_only: false, - } - } - - /// Expands the package list. - /// - /// This resolves and expands all rebuild groups. - pub fn expand_packages(&self, workspace: &Workspace) -> Result> { - let mut out = vec![]; - let tree = workspace.directory().join("TREE"); - for pkg in &self.packages { - if pkg.starts_with("groups/") { - let path = tree.join(pkg); - let nested = read_package_list(&tree, &path, 1)?; - out.extend(nested); - } else { - out.push(pkg.to_owned()); - } - } - Ok(out) - } - - /// Executes the build in a container. - pub fn execute(self, container: &Container) -> BuildResult { - BuildCheckPoint::from(self, container.workspace()) - .map_err(|err| (None, err))? - .execute(container) - } -} - -fn read_package_list>(tree: P, file: P, depth: usize) -> Result> { - if depth > 32 { - return Err(Error::NestedPackageGroup); - } - let f = fs::File::open(file)?; - let reader = BufReader::new(f); - let mut results = Vec::new(); - for line in reader.lines() { - let line = line?; - // skip comment - if line.starts_with('#') { - continue; - } - // trim whitespace - let trimmed = line.trim(); - // skip empty line - if trimmed.is_empty() { - continue; - } - // process nested groups - if trimmed.starts_with("groups/") { - let path = tree.as_ref().join(trimmed); - let nested = read_package_list(tree.as_ref(), &path, depth + 1)?; - results.extend(nested); - continue; - } - results.push(trimmed.to_owned()); - } - - Ok(results) -} - -/// A build checkpopint, including all packages to build and build progress. -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct BuildCheckPoint { - /// The original build request. - pub build: BuildRequest, - /// Expanded target packages list. - pub packages: Vec, - /// Built packages index, starting from zero - pub progress: usize, - /// Elapsed time in seconds - pub time_elapsed: u64, - /// Retry attempts - pub attempts: usize, -} - -impl BuildCheckPoint { - /// Loads a build checkpoint. - pub fn load>(path: P) -> Result { - Ok(bincode::deserialize(&fs::read(path)?)?) - } - - /// Writes a build checkpoint to file. - pub fn write>(&self, path: P) -> Result<()> { - fs::write(path, self.serialize()?)?; - Ok(()) - } - - /// Serializes a build checkpoint in bincode format. - pub fn serialize(&self) -> Result> { - Ok(bincode::serialize(self)?) - } -} - -#[derive(thiserror::Error, Debug)] -pub enum BuildError { - #[error(transparent)] - CielError(#[from] crate::Error), - #[error("Failed to expand package list: {0}")] - GroupExpansionFailure(crate::Error), - #[error("Failed to update build container: {0}")] - UpdateFailure(crate::Error), - #[error("acbs-build exied with error: {0}")] - AcbsFailure(ExitStatus), - #[error("Failed to refresh the package repository: {0}")] - RefreshRepoError(crate::Error), -} - -impl BuildError { - /// Converts build errors into [crate::Error] - pub fn into_ciel_error(self) -> Option { - match self { - BuildError::CielError(error) - | BuildError::GroupExpansionFailure(error) - | BuildError::UpdateFailure(error) - | BuildError::RefreshRepoError(error) => Some(error), - BuildError::AcbsFailure(status) => Some(crate::Error::SubcommandError(status)), - } - } - - /// Converts build errors into exit statuses - pub fn into_exit_status(self) -> Option { - self.into_ciel_error().and_then(|err| match err { - Error::SubcommandError(status) => Some(status), - _ => None, - }) - } -} - -/// Output of a build request. -#[derive(Debug, Clone)] -pub struct BuildOutput { - /// Number of built packages. - pub total_packages: usize, - /// Total elapsed time, in seconds. - pub time_elapsed: u64, -} - -pub type BuildResult = std::result::Result, BuildError)>; - -impl BuildCheckPoint { - /// Creates a checkpoint from build request, marking all packages as not built yet. - pub fn from( - request: BuildRequest, - workspace: &Workspace, - ) -> std::result::Result { - Ok(Self { - build: request.clone(), - packages: request - .expand_packages(workspace) - .map_err(|err| BuildError::GroupExpansionFailure(err))?, - progress: 0, - time_elapsed: 0, - attempts: 0, - }) - } - - /// Resumes the build in a container. - pub fn execute(mut self, container: &Container) -> BuildResult { - info!("Executing build: {:?}", self.build); - self.attempts += 1; - - let start = Instant::now(); - match execute(&mut self, container) { - Ok(mut out) => { - out.time_elapsed += start.elapsed().as_secs(); - Ok(out) - } - Err(err) => { - self.time_elapsed += start.elapsed().as_secs(); - Err((Some(self), err)) - } - } - } -} - -fn execute( - ckpt: &mut BuildCheckPoint, - container: &Container, -) -> std::result::Result { - let outupt_dir = container.output_directory(); - let total = ckpt.packages.len(); - - let hostname = gethostname().map_or_else( - |_| "unknown".to_string(), - |s| s.into_string().unwrap_or_else(|_| "unknown".to_string()), - ); - let refresh_monitor = RepositoryRefreshMonitor::new(SimpleAptRepository::new(&outupt_dir)); - - for (index, package) in ckpt.packages.iter().enumerate() { - if index < ckpt.progress { - continue; - } - // set terminal title, \r is for hiding the message if the terminal does not support the sequence - eprint!( - "\x1b]0;ciel: [{}/{}] {} ({}@{})\x07\r", - index + 1, - total, - package, - container.instance().name(), - hostname - ); - info!("[{}/{}] Building {} ...", index + 1, total, package); - container.rollback()?; - container.boot()?; - - info!("Refreshing local repository ..."); - SimpleAptRepository::new(&outupt_dir).refresh()?; - - { - let mut apt = None; - for i in 1..=5 { - match container.machine()?.update_system(apt) { - Ok(()) => break, - Err(Error::SubcommandError(status)) => { - if i == 5 { - return Err(BuildError::UpdateFailure(Error::SubcommandError(status))); - } - let interval = 3u64.pow(i); - warn!( - "Failed to update the OS, will retry in {} seconds ...", - interval - ); - apt = Some(true); - std::thread::sleep(Duration::from_secs(interval)); - } - Err(err) => return Err(BuildError::UpdateFailure(err)), - } - } - } - - let mut args = vec!["/usr/bin/acbs-build"]; - if ckpt.build.fetch_only { - args.push("-g"); - } - args.push("--"); - args.push(&package); - let status = container.machine()?.exec(args)?; - if !status.success() { - return Err(BuildError::AcbsFailure(status)); - } - ckpt.progress = index; - } - - refresh_monitor - .stop() - .map_err(|err| BuildError::RefreshRepoError(err))?; - Ok(BuildOutput { - total_packages: total, - time_elapsed: ckpt.time_elapsed, - }) -} - -pub trait BuildExt { - fn build(&self, build: BuildRequest) -> BuildResult; - fn resume(&self, ckpt: BuildCheckPoint) -> BuildResult; -} - -impl BuildExt for Container { - fn build(&self, build: BuildRequest) -> BuildResult { - build.execute(&self) - } - fn resume(&self, ckpt: BuildCheckPoint) -> BuildResult { - ckpt.execute(&self) - } -} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..87ad806 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,195 @@ +use anyhow::{anyhow, Result}; +use clap::{Arg, Command}; +use std::ffi::OsStr; + +pub const GIT_TREE_URL: &str = "https://github.com/AOSC-Dev/aosc-os-abbs.git"; + +/// List all the available plugins/helper scripts +fn list_helpers() -> Result> { + let exe_dir = std::env::current_exe().and_then(std::fs::canonicalize)?; + let exe_dir = exe_dir.parent().ok_or_else(|| anyhow!("Where am I?"))?; + let plugins_dir = exe_dir.join("../libexec/ciel-plugin/").read_dir()?; + let plugins = plugins_dir + .filter_map(|x| { + if let Ok(x) = x { + let path = x.path(); + let filename = path + .file_name() + .unwrap_or_else(|| OsStr::new("")) + .to_string_lossy(); + if path.is_file() && filename.starts_with("ciel-") { + return Some(filename.to_string()); + } + } + None + }) + .collect(); + + Ok(plugins) +} + +/// Build the CLI instance +pub fn build_cli() -> Command { + let instance_arg = Arg::new("INSTANCE") + .short('i') + .num_args(1) + .env("CIEL_INST") + .action(clap::ArgAction::Set); + Command::new("ciel") + .version(env!("CARGO_PKG_VERSION")) + .about("CIEL! is a nspawn container manager") + .allow_external_subcommands(true) + .subcommand(Command::new("version").about("Display the version of CIEL!")) + .subcommand(Command::new("init") + .arg(Arg::new("upgrade").long("upgrade").action(clap::ArgAction::SetTrue).help("Upgrade Ciel workspace from an older version")) + .about("Initialize the work directory")) + .subcommand( + Command::new("load-os") + .arg(Arg::new("url").help("URL or path to the tarball")) + .arg(Arg::new("arch").short('a').long("arch").help("Specify the target architecture for fetching OS tarball")) + .about("Unpack OS tarball or fetch the latest BuildKit from the repository"), + ) + .subcommand( + Command::new("update-os") + .arg(Arg::new("force_use_apt").long("force-use-apt").help("Use apt to update-os").action(clap::ArgAction::SetTrue)) + .about("Update the OS in the container") + ) + .subcommand( + Command::new("load-tree") + .arg(Arg::new("url").default_value(GIT_TREE_URL).help("URL to the git repository")) + .about("Clone package tree from the link provided or AOSC OS ABBS main repository"), + ) + .subcommand( + Command::new("update-tree") + .arg(Arg::new("rebase").num_args(1).short('r').long("rebase").help("Rebase the specified branch from the updated upstream")) + .arg(Arg::new("branch").num_args(1).help("Branch to switch to")) + .about("Update the existing ABBS tree (fetch only) and optionally switch to a different branch") + ) + .subcommand( + Command::new("new") + .arg(Arg::new("tarball").num_args(1).long("from-tarball").help("Create a new workspace from the specified tarball")) + .arg(Arg::new("arch").num_args(1).short('a').long("arch").help("Create a new workspace for specified architecture")) + .about("Create a new CIEL workspace") + ) + .subcommand( + Command::new("list") + .alias("ls") + .about("List all the instances under the specified working directory"), + ) + .subcommand( + Command::new("add") + .arg(Arg::new("INSTANCE").required(true)) + .about("Add a new instance"), + ) + .subcommand( + Command::new("del") + .alias("rm") + .arg(Arg::new("INSTANCE").required(true)) + .about("Remove an instance"), + ) + .subcommand( + Command::new("shell") + .alias("sh") + .arg(instance_arg.clone().help("Instance to be used")) + .arg(Arg::new("COMMANDS").required(false).num_args(1..)) + .about("Start an interactive shell"), + ) + .subcommand( + Command::new("run") + .alias("exec") + .arg(instance_arg.clone().help("Instance to run command in")) + .arg(Arg::new("COMMANDS").required(true).num_args(1..)) + .about("Lower-level version of 'shell', without login environment, without sourcing ~/.bash_profile"), + ) + .subcommand( + Command::new("config") + .arg(instance_arg.clone().help("Instance to be configured")) + .arg(Arg::new("g").short('g').action(clap::ArgAction::SetTrue).conflicts_with("INSTANCE").help("Configure base system instead of an instance")) + .about("Configure system and toolchain for building interactively"), + ) + .subcommand( + Command::new("commit") + .arg(instance_arg.clone().help("Instance to be committed")) + .about("Commit changes onto the shared underlying OS"), + ) + .subcommand( + Command::new("doctor") + .about("Diagnose problems (hopefully)"), + ) + .subcommand( + Command::new("build") + .arg(Arg::new("FETCH").short('g').action(clap::ArgAction::SetTrue).help("Fetch source packages only")) + .arg(Arg::new("OFFLINE").short('x').long("offline").action(clap::ArgAction::SetTrue).env("CIEL_OFFLINE").help("Disable network in the container during the build")) + .arg(instance_arg.clone().help("Instance to build in")) + .arg(Arg::new("STAGE2").long("stage2").short('2').action(clap::ArgAction::SetTrue).env("CIEL_STAGE2").help("Use stage 2 mode instead of the regular build mode")) + .arg(Arg::new("CONTINUE").conflicts_with("SELECT").short('c').long("resume").alias("continue").num_args(1).help("Continue from a Ciel checkpoint")) + .arg(Arg::new("SELECT").num_args(0..=1).long("stage-select").help("Select the starting point for a build")) + .arg(Arg::new("PACKAGES").conflicts_with("CONTINUE").num_args(1..)) + .about("Build the packages using the specified instance"), + ) + .subcommand( + Command::new("rollback") + .arg(instance_arg.clone().help("Instance to be rolled back")) + .about("Rollback all or specified instance"), + ) + .subcommand( + Command::new("down") + .alias("umount") + .arg(instance_arg.clone().help("Instance to be un-mounted")) + .about("Shutdown and unmount all or one instance"), + ) + .subcommand( + Command::new("stop") + .arg(instance_arg.clone().help("Instance to be stopped")) + .about("Shuts down an instance"), + ) + .subcommand( + Command::new("mount") + .arg(instance_arg.help("Instance to be mounted")) + .about("Mount all or specified instance"), + ) + .subcommand( + Command::new("farewell") + .alias("harakiri") + .about("Remove everything related to CIEL!"), + ) + .subcommand( + Command::new("repo") + .arg_required_else_help(true) + .subcommands(vec![Command::new("refresh").about("Refresh the repository"), Command::new("init").arg(Arg::new("INSTANCE").required(true)).about("Initialize the repository"), Command::new("deinit").about("Uninitialize the repository")]) + .alias("localrepo") + .about("Local repository operations") + ) + .subcommand( + Command::new("clean") + .about("Clean all the output directories and source cache directories") + ) + .subcommands({ + let plugins = list_helpers(); + if let Ok(plugins) = plugins { + plugins.iter().map(|plugin| { + let name = plugin.strip_prefix("ciel-").unwrap_or("???"); + Command::new(name.to_string()) + .arg(Arg::new("COMMANDS").required(false).num_args(1..).help("Applet specific commands")) + .about("") + }).collect() + } else { + vec![] + } + }) + .args( + &[ + Arg::new("C") + .short('C') + .value_name("DIR") + .default_value(".") + .num_args(1..) + .help("Set the CIEL! working directory"), + Arg::new("batch") + .short('b') + .long("batch") + .action(clap::ArgAction::SetTrue) + .help("Batch mode, no input required"), + ] + ) +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..48eeb54 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,241 @@ +use anyhow::{anyhow, Result}; +use console::user_attended; +use dialoguer::{theme::ColorfulTheme, FuzzySelect}; +use indicatif::ProgressBar; +use sha2::{Digest, Sha256}; +use std::env::consts::ARCH; +use std::fs::{self, File}; +use std::os::unix::prelude::MetadataExt; +use std::sync::LazyLock; +use std::{ + io::{Read, Write}, + path::{Path, PathBuf}, + time::Duration, +}; +use unsquashfs_wrapper::Unsquashfs; + +pub const CIEL_MAINLINE_ARCHS: &[&str] = &[ + "amd64", + "arm64", + "ppc64el", + "mips64r6el", + "riscv64", + "loongarch64", + "loongson3", +]; +pub const CIEL_RETRO_ARCHS: &[&str] = &["armv4", "armv6hf", "armv7hf", "i486", "m68k", "powerpc"]; +pub const CURRENT_CIEL_VERSION: usize = 3; +const CURRENT_CIEL_VERSION_STR: &str = "3"; +pub const CIEL_DIST_DIR: &str = ".ciel/container/dist"; +pub const CIEL_INST_DIR: &str = ".ciel/container/instances"; +pub const CIEL_DATA_DIR: &str = ".ciel/data"; +const SKELETON_DIRS: &[&str] = &[CIEL_DIST_DIR, CIEL_INST_DIR, CIEL_DATA_DIR]; + +static SPINNER_STYLE: LazyLock = LazyLock::new(|| { + indicatif::ProgressStyle::default_spinner() + .tick_chars("⠋⠙⠸⠴⠦⠇ ") + .template("{spinner:.green} {wide_msg}") + .unwrap() +}); + +#[macro_export] +macro_rules! make_progress_bar { + ($msg:expr) => { + concat!( + "{spinner} [{bar:25.cyan/blue}] ", + $msg, + " ({bytes_per_sec}, eta {eta})" + ) + }; +} + +#[inline] +pub fn create_spinner(msg: &'static str, tick_rate: u64) -> indicatif::ProgressBar { + let spinner = indicatif::ProgressBar::new_spinner().with_style(SPINNER_STYLE.clone()); + spinner.set_message(msg); + spinner.enable_steady_tick(Duration::from_millis(tick_rate)); + + spinner +} + +#[inline] +pub fn check_arch_name(arch: &str) -> bool { + CIEL_MAINLINE_ARCHS.contains(&arch) || CIEL_RETRO_ARCHS.contains(&arch) +} + +/// AOSC OS specific architecture mapping table +#[inline] +pub fn get_host_arch_name() -> Option<&'static str> { + #[cfg(not(target_arch = "powerpc64"))] + match ARCH { + "x86_64" => Some("amd64"), + "x86" => Some("i486"), + "powerpc" => Some("powerpc"), + "aarch64" => Some("arm64"), + "mips64" => Some("loongson3"), + "riscv64" => Some("riscv64"), + "loongarch64" => Some("loongarch64"), + _ => None, + } + + #[cfg(target_arch = "powerpc64")] + { + let mut endian: libc::c_int = -1; + let result = unsafe { libc::prctl(libc::PR_GET_ENDIAN, &mut endian as *mut libc::c_int) }; + if result < 0 { + return None; + } + match endian { + libc::PR_ENDIAN_LITTLE | libc::PR_ENDIAN_PPC_LITTLE => Some("ppc64el"), + libc::PR_ENDIAN_BIG => Some("ppc64"), + _ => None, + } + } +} + +/// Calculate the Sha256 checksum of the given stream +pub fn sha256sum(mut reader: R) -> Result { + let mut hasher = Sha256::new(); + std::io::copy(&mut reader, &mut hasher)?; + + Ok(format!("{:x}", hasher.finalize())) +} + +/// Extract the given .tar.xz stream and preserve all the file attributes +pub fn extract_tar_xz(reader: R, path: &Path) -> Result<()> { + let decompress = xz2::read::XzDecoder::new(reader); + let mut tar_processor = tar::Archive::new(decompress); + tar_processor.set_unpack_xattrs(true); + tar_processor.set_preserve_permissions(true); + tar_processor.unpack(path)?; + + Ok(()) +} + +/// Extract the given .squashfs +pub fn extract_squashfs(path: &Path, dist_dir: &Path, pb: &ProgressBar, total: u64) -> Result<()> { + let unsquashfs = Unsquashfs::default(); + + unsquashfs.extract(path, dist_dir, None, move |c| { + pb.set_position(total * c as u64 / 100); + })?; + + Ok(()) +} + +pub fn extract_system_rootfs(path: &Path, total: u64, use_tarball: bool) -> Result<()> { + let f = File::open(path)?; + let progress_bar = indicatif::ProgressBar::new(total); + + progress_bar.set_style( + indicatif::ProgressStyle::default_bar() + .template(make_progress_bar!("Extracting rootfs ...")) + .unwrap(), + ); + + progress_bar.set_draw_target(indicatif::ProgressDrawTarget::stderr_with_hz(5)); + + let dist_dir = PathBuf::from(CIEL_DIST_DIR); + if dist_dir.exists() { + fs::remove_dir_all(&dist_dir).ok(); + fs::create_dir_all(&dist_dir)?; + } + + // detect if we are running in systemd-nspawn + // where /dev/console character device file cannot be created + // thus ignoring the error in extracting + let mut in_systemd_nspawn = false; + if let Ok(output) = std::process::Command::new("systemd-detect-virt").output() { + if let Ok("systemd-nspawn") = std::str::from_utf8(&output.stdout) { + in_systemd_nspawn = true; + } + } + + let res = if use_tarball { + extract_tar_xz(progress_bar.wrap_read(f), &dist_dir) + } else { + extract_squashfs(path, &dist_dir, &progress_bar, total) + }; + + if !in_systemd_nspawn { + res? + } + + progress_bar.finish_and_clear(); + + Ok(()) +} + +pub fn ciel_init() -> Result<()> { + for dir in SKELETON_DIRS { + fs::create_dir_all(dir)?; + } + let mut f = File::create(".ciel/version")?; + f.write_all(CURRENT_CIEL_VERSION_STR.as_bytes())?; + + Ok(()) +} + +/// Find the ciel directory +pub fn find_ciel_dir>(start: P) -> Result { + let start_path = fs::metadata(start.as_ref())?; + let start_dev = start_path.dev(); + let mut current_dir = start.as_ref().to_path_buf(); + loop { + if !current_dir.exists() { + return Err(anyhow!("Hit filesystem ceiling!")); + } + let current_dev = current_dir.metadata()?.dev(); + if current_dev != start_dev { + return Err(anyhow!("Hit filesystem boundary!")); + } + if current_dir.join(".ciel").is_dir() { + return Ok(current_dir); + } + current_dir = current_dir.join(".."); + } +} + +pub fn is_instance_exists(instance: &str) -> bool { + Path::new(CIEL_INST_DIR).join(instance).is_dir() +} + +pub fn is_legacy_workspace() -> Result { + let mut f = fs::File::open(".ciel/version")?; + // TODO: use a more robust check + let mut buf = [0u8; 1]; + f.read_exact(&mut buf)?; + + Ok(buf[0] < CURRENT_CIEL_VERSION_STR.as_bytes()[0]) +} + +pub fn ask_for_target_arch() -> Result<&'static str> { + // Collect all supported architectures + let host_arch = get_host_arch_name(); + if !user_attended() { + return match host_arch { + Some(v) => Ok(v), + None => Err(anyhow!("Could not determine host architecture")), + }; + } + let mut all_archs: Vec<&'static str> = CIEL_MAINLINE_ARCHS.into(); + all_archs.append(&mut CIEL_RETRO_ARCHS.into()); + let default_arch_index = match host_arch { + Some(host_arch) => all_archs.iter().position(|a| *a == host_arch).unwrap(), + None => 0, + }; + // Setup Dialoguer + let theme = ColorfulTheme::default(); + let prefixed_archs = CIEL_MAINLINE_ARCHS + .iter() + .map(|x| format!("mainline: {x}")) + .chain(CIEL_RETRO_ARCHS.iter().map(|x| format!("retro: {x}"))) + .collect::>(); + let chosen_index = FuzzySelect::with_theme(&theme) + .with_prompt("Target Architecture") + .default(default_arch_index) + .items(prefixed_archs.as_slice()) + .interact()?; + + Ok(all_archs[chosen_index]) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..6524f44 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,271 @@ +//! This module contains configuration files related APIs + +use crate::common::CURRENT_CIEL_VERSION; +use crate::{get_host_arch_name, info}; +use anyhow::{anyhow, Result}; +use console::{style, user_attended}; +use dialoguer::{theme::ColorfulTheme, Confirm, Editor, Input}; +use serde::{Deserialize, Serialize}; +use std::{ffi::OsString, path::Path}; +use std::{ + fs, + io::{Read, Write}, +}; + +const DEFAULT_CONFIG_LOCATION: &str = ".ciel/data/config.toml"; +const DEFAULT_APT_SOURCE: &str = "deb https://repo.aosc.io/debs/ stable main"; +const DEFAULT_AB4_CONFIG_FILE: &str = "ab4cfg.sh"; +const DEFAULT_AB4_CONFIG_LOCATION: &str = "etc/autobuild/ab4cfg.sh"; +const DEFAULT_APT_LIST_LOCATION: &str = "etc/apt/sources.list"; +const DEFAULT_RESOLV_LOCATION: &str = "etc/systemd/resolved.conf"; +const DEFAULT_ACBS_CONFIG: &str = "etc/acbs/forest.conf"; + +#[derive(Debug, Serialize, Deserialize)] +pub struct CielConfig { + version: usize, + maintainer: String, + dnssec: bool, + apt_sources: String, + pub local_repo: bool, + pub local_sources: bool, + #[serde(rename = "nspawn-extra-options")] + pub extra_options: Vec, + #[serde(rename = "branch-exclusive-output")] + pub sep_mount: bool, + #[serde(rename = "volatile-mount", default)] + pub volatile_mount: bool, + #[serde(default = "CielConfig::default_force_use_apt")] + pub force_use_apt: bool, +} + +impl CielConfig { + const fn default_force_use_apt() -> bool { + cfg!(target_arch = "riscv64") + } + + pub fn save_config(&self) -> Result { + Ok(toml::to_string(self)?) + } + + pub fn load_config(data: &str) -> Result { + Ok(toml::from_str(data)?) + } +} + +impl Default for CielConfig { + fn default() -> Self { + CielConfig { + version: CURRENT_CIEL_VERSION, + maintainer: "Bot ".to_string(), + dnssec: false, + apt_sources: DEFAULT_APT_SOURCE.to_string(), + local_repo: true, + local_sources: true, + extra_options: Vec::new(), + sep_mount: true, + volatile_mount: false, + force_use_apt: false, + } + } +} + +#[allow(clippy::ptr_arg)] +fn validate_maintainer(maintainer: &String) -> Result<(), String> { + let mut lt = false; // "<" + let mut gt = false; // ">" + let mut at = false; // "@" + let mut name = false; + let mut nbsp = false; // space + // A simple FSM to match the states + for c in maintainer.as_bytes() { + match *c { + b'<' => { + if !nbsp { + return Err("Please enter a name.".to_owned()); + } + lt = true; + } + b'>' => { + if !lt { + return Err("Invalid format.".to_owned()); + } + gt = true; + } + b'@' => { + if !lt || gt { + return Err("Invalid format.".to_owned()); + } + at = true; + } + b' ' | b'\t' => { + if !name { + return Err("Please enter a name.".to_owned()); + } + nbsp = true; + } + _ => { + if !nbsp { + name = true; + continue; + } + } + } + } + + if name && gt && lt && at { + return Ok(()); + } + + Err("Invalid format.".to_owned()) +} + +#[inline] +fn create_parent_dir(path: &Path) -> Result<()> { + let path = path + .parent() + .ok_or_else(|| anyhow!("Parent directory is root."))?; + fs::create_dir_all(path)?; + + Ok(()) +} + +#[inline] +fn get_default_editor() -> OsString { + if let Some(prog) = std::env::var_os("VISUAL") { + return prog; + } + if let Some(prog) = std::env::var_os("EDITOR") { + return prog; + } + if let Ok(editor) = which::which("editor") { + return editor.as_os_str().to_os_string(); + } + + "nano".into() +} + +/// Shows a series of prompts to let the user select the configurations +pub fn ask_for_config(config: Option) -> Result { + let mut config = config.unwrap_or_default(); + if !user_attended() { + info!("Not controlled by an user. Default values are used."); + return Ok(config); + } + let theme = ColorfulTheme::default(); + config.maintainer = Input::::with_theme(&theme) + .with_prompt("Maintainer Information") + .default(config.maintainer) + .validate_with(validate_maintainer) + .interact_text()?; + config.dnssec = Confirm::with_theme(&theme) + .with_prompt("Enable DNSSEC") + .default(config.dnssec) + .interact()?; + let edit_source = Confirm::with_theme(&theme) + .with_prompt("Edit sources.list") + .default(false) + .interact()?; + if edit_source { + config.apt_sources = Editor::new() + .executable(get_default_editor()) + .extension(".list") + .edit(if config.apt_sources.is_empty() { + DEFAULT_APT_SOURCE + } else { + &config.apt_sources + })? + .unwrap_or_else(|| DEFAULT_APT_SOURCE.to_owned()); + } + config.local_sources = Confirm::with_theme(&theme) + .with_prompt("Enable local sources caching") + .default(config.local_sources) + .interact()?; + config.local_repo = Confirm::with_theme(&theme) + .with_prompt("Enable local packages repository") + .default(config.local_repo) + .interact()?; + config.sep_mount = Confirm::with_theme(&theme) + .with_prompt("Use different OUTPUT dir for different branches") + .default(config.sep_mount) + .interact()?; + config.volatile_mount = Confirm::with_theme(&theme) + .with_prompt("Use volatile mode for filesystem operations") + .default(config.volatile_mount) + .interact()?; + + // FIXME: RISC-V build hosts is unreliable when using oma: random lock-ups + // during `oma refresh'. Disabling oma to workaround potential lock-ups. + if get_host_arch_name().map(|x| x != "riscv64").unwrap_or(true) { + info!("Ciel now uses oma as the default package manager for base system updating tasks."); + info!("You can choose whether to use oma instead of apt while configuring."); + config.force_use_apt = Confirm::with_theme(&theme) + .with_prompt("Use apt as package manager") + .default(config.force_use_apt) + .interact()?; + } + + Ok(config) +} + +/// Reads the configuration file from the current workspace +pub fn read_config() -> Result { + let mut f = std::fs::File::open(DEFAULT_CONFIG_LOCATION)?; + let mut data = String::new(); + f.read_to_string(&mut data)?; + + CielConfig::load_config(&data) +} + +/// Applies the given configuration (th configuration itself will not be saved to the disk) +pub fn apply_config>(root: P, config: &CielConfig) -> Result<()> { + // write maintainer information + let rootfs = root.as_ref(); + let mut config_path = rootfs.to_owned(); + config_path.push(DEFAULT_AB4_CONFIG_LOCATION); + create_parent_dir(&config_path)?; + let mut f = std::fs::File::create(&config_path)?; + f.write_all( + format!( + "#!/bin/bash\nABMPM=dpkg\nABAPMS=\nABINSTALL=dpkg\nMTER=\"{}\"", + config.maintainer + ) + .as_bytes(), + )?; + config_path.set_file_name(DEFAULT_AB4_CONFIG_FILE); + // write sources.list + if !config.apt_sources.is_empty() { + let mut apt_list_path = rootfs.to_owned(); + apt_list_path.push(DEFAULT_APT_LIST_LOCATION); + create_parent_dir(&apt_list_path)?; + let mut f = std::fs::File::create(apt_list_path)?; + f.write_all(config.apt_sources.as_bytes())?; + } + // write DNSSEC configuration + if !config.dnssec { + let mut resolv_path = rootfs.to_owned(); + resolv_path.push(DEFAULT_RESOLV_LOCATION); + create_parent_dir(&resolv_path)?; + let mut f = std::fs::File::create(resolv_path)?; + f.write_all(b"[Resolve]\nDNSSEC=no\n")?; + } + // write acbs configuration + let mut acbs_path = rootfs.to_owned(); + acbs_path.push(DEFAULT_ACBS_CONFIG); + create_parent_dir(&acbs_path)?; + let mut f = std::fs::File::create(acbs_path)?; + f.write_all(b"[default]\nlocation = /tree/\n")?; + + Ok(()) +} + +#[test] +fn test_validate_maintainer() { + assert_eq!( + validate_maintainer(&"test ".to_owned()), + Ok(()) + ); + assert_eq!( + validate_maintainer(&"test , - #[allow(unused)] - lock: Arc, - ns_name: String, - - rootfs_path: PathBuf, - config_path: PathBuf, - upper_layer: BoxedLayer, - lower_layers: Arc>, - overlay_mgr: Arc>>, - machine: Arc>, -} - -impl PartialEq for Container { - fn eq(&self, other: &Self) -> bool { - self.instance == other.instance - } -} - -impl AsRef for Container { - #[inline(always)] - fn as_ref(&self) -> &Self { - self - } -} - -struct FileLock(File); - -impl FileLock { - /// Unlocks the locked file forcibly. - pub fn force_unlock(&self) { - fs3::FileExt::unlock(&self.0).unwrap(); - } -} - -impl Drop for FileLock { - fn drop(&mut self) { - fs3::FileExt::unlock(&self.0).unwrap(); - } -} - -impl Container { - /// Opens the build container, locking it exclusively. - pub fn open(instance: Instance) -> Result { - let lock = File::options() - .read(true) - .write(true) - .create(true) - .open(instance.directory().join(".lock"))?; - fs3::FileExt::lock_exclusive(&lock)?; - let lock = FileLock(lock); - - let ns_name = make_container_ns_name(instance.name())?; - let rootfs_path = instance.workspace().directory().join(instance.name()); - - let config_snapshot = rootfs_path.join(".ciel.toml"); - let config = if config_snapshot.exists() { - ContainerConfig::load(config_snapshot)? - } else { - ContainerConfig { - instance_name: instance.name().to_owned(), - ns_name: ns_name.to_owned(), - workspace_config: instance.workspace().config(), - instance_config: instance.config(), - } - }; - - let upper_dir = instance.directory().join("layers/upper"); - let upper_layer: BoxedLayer = if let Some(tmpfs) = &config.instance_config.tmpfs { - Arc::new(Box::new(TmpfsLayer::new(&upper_dir, tmpfs))) - } else { - Arc::new(Box::new(SimpleLayer::new(&upper_dir))) - }; - let config_path = instance.directory().join("layers/local"); - let lower_layers: Vec = vec![ - Arc::new(Box::new(TmpfsLayer::new( - &config_path, - &TmpfsConfig { size: Some(16) }, - ))), - Arc::new(Box::new(SimpleLayer::from( - instance.workspace().system_rootfs(), - ))), - ]; - - Ok(Self { - instance, - config: Arc::new(config), - lock: Arc::new(lock), - ns_name, - rootfs_path, - config_path, - upper_layer, - lower_layers: Arc::new(lower_layers), - overlay_mgr: Arc::default(), - machine: Arc::default(), - }) - } - - /// Returns the [Instance] object. - pub fn instance(&self) -> &Instance { - &self.instance - } - - /// Returns the [Workspace] object. - pub fn workspace(&self) -> &Workspace { - &self.instance.workspace() - } - - /// Returns the instance directory. - pub fn directory(&self) -> &Path { - self.instance.directory() - } - - /// Returns the container configuration snapshot. - pub fn config(&self) -> &ContainerConfig { - &self.config - } - - /// Returns the NS name of the container. - pub fn as_ns_name(&self) -> &str { - &self.ns_name - } - - /// Returns the path to the root filesystem of the container. - pub fn rootfs_path(&self) -> &Path { - &self.rootfs_path - } - - /// Returns the path to the configuration layer of the container. - pub fn config_path(&self) -> &Path { - &self.config_path - } - - /// Returns the upper layer of filesystem. - /// - /// The upper layer is for layer managers to place ephemeral contents. - /// - /// Note that the upper-layer structure is not guaranteed. - /// Thus you should avoid writing files into upper layer directly. - /// Instead, write into [Container::rootfs_path]. - pub fn upper_layer(&self) -> BoxedLayer { - self.upper_layer.to_owned() - } - - /// Returns the lower layers of filesystem. - pub fn lower_layers(&self) -> impl Iterator + use<'_> { - self.lower_layers.iter().cloned() - } - - /// Returns the [OverlayManager] object. - pub fn overlay_manager(&self) -> &Box { - &self.overlay_mgr.get_or_init(|| { - Box::new(if self.instance.directory().join("diff").exists() { - OverlayFS::new_compat( - self.rootfs_path.to_owned(), - self.instance.directory().join("layers"), - self.lower_layers.to_vec(), - self.config.workspace_config.volatile_mount, - ) - } else { - OverlayFS::new( - self.rootfs_path.as_path(), - self.upper_layer.to_owned(), - self.lower_layers.to_vec(), - self.config.workspace_config.volatile_mount, - ) - }) - }) - } - - /// Returns the [Machine] object. - pub fn machine(&self) -> Result<&Machine> { - // FIXME: use get_or_try_init after stablization - if let Some(machine) = self.machine.get() { - Ok(machine) - } else { - let machine = Machine::new(self.config.to_owned(), self.rootfs_path.to_owned())?; - _ = self.machine.set(machine); - Ok(self.machine.get().unwrap()) - } - } - - /// Returns the state of container - pub fn state(&self) -> Result { - if self.overlay_manager().is_mounted()? { - Ok(match self.machine()?.state()? { - MachineState::Down => ContainerState::Mounted, - MachineState::Starting => ContainerState::Starting, - MachineState::Running => ContainerState::Running, - }) - } else { - Ok(ContainerState::Down) - } - } - - /// Boots this container. - pub fn boot(&self) -> Result<()> { - let state = self.state()?; - - if !state.is_mounted() { - self.overlay_manager().mount()?; - setup_container(&self)?; - } - - if !matches!(state, ContainerState::Starting | ContainerState::Running) { - self.machine()?.boot()?; - setup_machine(&self)?; - } - - Ok(()) - } - - /// Stops this container. - pub fn stop(&self, unmount: bool) -> Result<()> { - let state = self.state()?; - - if matches!(state, ContainerState::Starting | ContainerState::Running) { - self.machine()?.stop()?; - } - - if unmount { - self.overlay_manager().unmount()?; - } - - Ok(()) - } - - /// Rollbacks the container. - /// - /// The container will be in Down state after rollback. - pub fn rollback(&self) -> Result<()> { - self.stop(true)?; - self.overlay_manager().rollback()?; - nix::unistd::sync(); - Ok(()) - } - - /// Returns the output directory of the container. - /// - /// If [InstanceConfig::output] is set, it will be preferred. - /// Or else [Workspace::output_directory] will be used. - pub fn output_directory(&self) -> PathBuf { - self.instance() - .config() - .output - .unwrap_or_else(|| self.workspace().output_directory()) - } -} - -impl TryFrom<&Instance> for Container { - type Error = crate::Error; - - fn try_from(value: &Instance) -> std::result::Result { - value.open() - } -} - -impl Debug for Container { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(&self.instance, f) - } -} - -/// Generates the NS name for a container. -/// -/// In version 3 workspaces, container names are in the following format: -/// `$name-adler32($absolute path)` -pub fn make_container_ns_name>(path: P) -> Result { - let path = path.as_ref(); - let hash = adler32::adler32(path::absolute(path)?.as_os_str().as_bytes())?; - let name = path - .file_name() - .and_then(|name| name.to_str()) - .ok_or_else(|| Error::InvalidInstancePath(path.to_owned()))?; - Ok(format!("{}-{:x}", name, hash)) -} - -/// A container configuration. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub struct ContainerConfig { - pub instance_name: String, - pub ns_name: String, - pub workspace_config: WorkspaceConfig, - pub instance_config: InstanceConfig, -} - -impl ContainerConfig { - /// The default path for container configuration. - pub const PATH: &str = "config.toml"; - - /// The current version of container configuration format. - pub const CURRENT_VERSION: usize = 3; - - /// Loads a container configuration from a given file path. - pub fn load>(path: P) -> Result { - let path = path.as_ref().to_path_buf(); - if path.exists() { - fs::read_to_string(&path)?.as_str().try_into() - } else { - Err(Error::ConfigNotFound(path)) - } - } - - /// Deserializes a container configuration TOML. - pub fn parse(config: &str) -> Result { - let config = toml::from_str::(config)?; - Ok(config) - } - - /// Serializes a container configuration into TOML. - pub fn serialize(&self) -> Result { - Ok(toml::to_string_pretty(&self)?) - } -} - -impl TryFrom<&str> for ContainerConfig { - type Error = crate::Error; - - fn try_from(value: &str) -> std::result::Result { - Self::parse(value) - } -} - -impl TryFrom<&ContainerConfig> for String { - type Error = crate::Error; - - fn try_from(value: &ContainerConfig) -> std::result::Result { - value.serialize() - } -} - -impl ContainerConfig { - /// Returns all APT repositories that should be available in containers. - /// - /// This includes the stable repository (`deb https://repo.aosc.io/debs/ stable main`) - /// and repositories from [WorkspaceConfig::extra_apt_repos] and [InstanceConfig::extra_apt_repos]. - /// If local repository is set to be included ([WorkspaceConfig::use_local_repo] - /// and [InstanceConfig::use_local_repo]), - /// `deb [trusted=yes] file:///debs/ /` will also be included. - pub fn all_apt_repos(&self) -> Vec { - let mut repos = vec!["deb https://repo.aosc.io/debs/ stable main".to_string()]; - repos.extend(self.workspace_config.extra_apt_repos.iter().cloned()); - repos.extend(self.instance_config.extra_apt_repos.iter().cloned()); - if self.workspace_config.use_local_repo && self.instance_config.use_local_repo { - repos.push("deb [trusted=yes] file:///debs/ /".to_string()); - } - repos - } -} - -/// The state of a container. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub enum ContainerState { - /// The container is down, with its filesystem un-mounted. - Down, - /// The container is mounted, but not started. - Mounted, - /// The container is starting. - Starting, - /// The container is booted. - Running, -} - -impl ContainerState { - pub fn is_down(&self) -> bool { - matches!(self, Self::Down) - } - - pub fn is_dirty(&self) -> bool { - !matches!(self, Self::Down) - } - - pub fn is_mounted(&self) -> bool { - matches!(self, Self::Mounted) - } - - pub fn is_starting(&self) -> bool { - matches!(self, Self::Starting) - } - - pub fn is_running(&self) -> bool { - matches!(self, Self::Running) - } -} - -impl From for ContainerState { - fn from(value: MachineState) -> Self { - match value { - MachineState::Down => Self::Down, - MachineState::Starting => Self::Starting, - MachineState::Running => Self::Running, - } - } -} - -fn setup_container(container: &Container) -> Result<()> { - let config_layer = &container.config_path(); - let workspace_config = &container.config.workspace_config; - // let instance_config = &container.config.instance_config; - - fn create_parent_dirs>(path: P) -> Result<()> { - if let Some(parent) = path.as_ref().parent() { - fs::create_dir_all(parent)?; - } - Ok(()) - } - - info!( - "{}: configuring container (post-mount) ...", - container.ns_name - ); - - // ciel config - fs::write( - config_layer.join(".ciel.toml"), - container.config.serialize()?, - )?; - - // autobuild4 configuration - let config_path = config_layer.join("etc/autobuild/ab4cfg.sh"); - create_parent_dirs(&config_path)?; - fs::write( - config_path, - format!( - "#!/bin/bash -ABMPM=dpkg -ABAPMS= -ABINSTALL=dpkg -MTER=\"{}\"", - workspace_config.maintainer - ), - )?; - - // APT sources - let apt_sources = container.config().all_apt_repos().join("\n"); - let apt_list_path = config_layer.join("etc/apt/sources.list"); - create_parent_dirs(&apt_list_path)?; - fs::write(apt_list_path, apt_sources)?; - - // DNSSEC configuration - if !workspace_config.dnssec { - let resolv_path = config_layer.join("etc/systemd/resolved.conf"); - create_parent_dirs(&resolv_path)?; - fs::write(resolv_path, "[Resolve]\nDNSSEC=no\n")?; - } - - // acbs configuration - let acbs_path = config_layer.join("etc/acbs/forest.conf"); - create_parent_dirs(&acbs_path)?; - fs::write(acbs_path, "[default]\nlocation = /tree/\n")?; - - // git config - let gitconfig_path = config_layer.join("root/.gitconfig"); - create_parent_dirs(&gitconfig_path)?; - fs::write(gitconfig_path, "[safe]\n\tdirectory = /tree\n")?; - - Ok(()) -} - -fn setup_machine(container: &Container) -> Result<()> { - let workspace_config = &container.config.workspace_config; - let instance_config = &container.config.instance_config; - let machine = container.machine()?; - let workspace_dir = container.workspace().directory(); - - info!( - "{}: configuring container (post-boot) ...", - container.ns_name - ); - - machine.bind( - workspace_dir.join("TREE"), - "/tree".into(), - instance_config.readonly_tree, - )?; - if !workspace_config.no_cache_packages { - machine.bind( - workspace_dir.join("CACHE"), - "/var/cache/apt/archives".into(), - false, - )?; - } - if workspace_config.cache_sources { - machine.bind( - workspace_dir.join("SRCS"), - "/var/cache/acbs/tarballs".into(), - false, - )?; - } - - let output = container.output_directory(); - info!( - "{}: using output directory: {} ...", - container.ns_name, - output.display() - ); - machine.bind(output, "/debs".into(), false)?; - - Ok(()) -} - -/// A owned container which will be destroyed automatically on drop. -#[derive(Debug)] -pub struct OwnedContainer(Container); - -impl OwnedContainer { - /// Leaks the owned container. - /// - /// This avoids the container being destroyed on drop. - #[must_use] - pub fn leak(self) -> Container { - let container = self.0.clone(); - forget(self); - container - } - - /// Destroies the owned container. - pub fn discard(self) -> Result<()> { - let instance = self.0.instance().to_owned(); - self.0.lock.force_unlock(); - forget(self); - instance.destroy() - } -} - -impl From for OwnedContainer { - fn from(value: Container) -> Self { - Self(value) - } -} - -impl AsRef for OwnedContainer { - fn as_ref(&self) -> &Container { - &self.0 - } -} - -impl Deref for OwnedContainer { - type Target = Container; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Drop for OwnedContainer { - fn drop(&mut self) { - let instance = self.0.instance().to_owned(); - self.0.lock.force_unlock(); - instance.destroy().unwrap(); - } -} - -#[cfg(test)] -mod test { - use crate::{ - container::{make_container_ns_name, OwnedContainer}, - test::TestDir, - Error, - }; - use test_log::test; - - #[test] - fn test_make_container_ns_name() { - assert_eq!( - make_container_ns_name("/home/xtex/src/aosc/ciel/a").unwrap(), - "a-80d90979" - ); - assert_eq!( - make_container_ns_name("/home/xtex/src/aosc/ciel/test").unwrap(), - "test-a0190ad8" - ); - assert_eq!( - make_container_ns_name("/buildroots/buildit/test").unwrap(), - "test-75210982" - ); - assert_eq!( - make_container_ns_name("/buildroots/mingcongbai/amd64/amd64").unwrap(), - "amd64-f1ac0cba" - ); - } - - #[test] - fn test_container_migration() { - let testdir = TestDir::from("testdata/old-workspace"); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - assert!(ws.is_system_loaded()); - let inst = ws.instance("test").unwrap(); - dbg!(&inst); - let container = inst.open().unwrap(); - dbg!(&container); - assert!(container.state().unwrap().is_down()); - } - - #[test] - fn test_container_state() { - let testdir = TestDir::from("testdata/simple-workspace"); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - assert!(ws.is_system_loaded()); - let inst = ws.instance("test").unwrap(); - dbg!(&inst); - let container = inst.open().unwrap(); - dbg!(&container); - assert!(container.state().unwrap().is_down()); - } - - #[test] - fn test_owned_container() { - let testdir = TestDir::from("testdata/simple-workspace"); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - assert!(ws.is_system_loaded()); - let inst = ws.instance("test").unwrap(); - dbg!(&inst); - let container = OwnedContainer::from(inst.open().unwrap()); - dbg!(&container); - assert!(container.state().unwrap().is_down()); - drop(container); - assert!(matches!( - ws.instance("test"), - Err(Error::InstanceNotFound(_)) - )) - } - - #[test] - fn test_owned_container_leak() { - let testdir = TestDir::from("testdata/simple-workspace"); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - assert!(ws.is_system_loaded()); - let inst = ws.instance("test").unwrap(); - dbg!(&inst); - let container = OwnedContainer::from(inst.open().unwrap()); - dbg!(&container); - assert!(container.state().unwrap().is_down()); - let container = container.leak(); - drop(container); - _ = ws.instance("test").unwrap(); - } - - #[test] - fn test_container_config_apt_repos() { - let testdir = TestDir::from("testdata/simple-workspace"); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - let inst = ws.instance("test").unwrap(); - let config = inst.open().unwrap().config().to_owned(); - assert_eq!( - config.all_apt_repos(), - vec![ - "deb https://repo.aosc.io/debs/ stable main".to_string(), - "deb file:///test/ test test".to_string(), - "deb file:///test test testinst".to_string(), - "deb [trusted=yes] file:///debs/ /".to_string(), - ] - ); - } -} diff --git a/src/dbus_machine1_manager.rs b/src/dbus_machine1.rs similarity index 77% rename from src/dbus_machine1_manager.rs rename to src/dbus_machine1.rs index 19255de..b577b9c 100644 --- a/src/dbus_machine1_manager.rs +++ b/src/dbus_machine1.rs @@ -1,28 +1,30 @@ -//! # D-Bus interface proxy for: `org.freedesktop.machine1.Manager` +//! # DBus interface proxy for: `org.freedesktop.machine1.Manager` //! -//! This code was generated by `zbus-xmlgen` `5.0.1` from D-Bus introspection data. -//! Source: `org.freedesktop.machine1-manager.xml`. +//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. +//! Source: `org.freedesktop.machine1.xml`. //! //! You may prefer to adapt it, instead of using it verbatim. //! -//! More information can be found in the [Writing a client proxy] section of the zbus -//! documentation. +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. //! -//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the -//! following zbus API can be used: +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: //! //! * [`zbus::fdo::PeerProxy`] //! * [`zbus::fdo::IntrospectableProxy`] //! * [`zbus::fdo::PropertiesProxy`] //! -//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. -//! -//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html -//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. + +#![allow(clippy::too_many_arguments)] +#![allow(clippy::type_complexity)] use zbus::proxy; + #[proxy( interface = "org.freedesktop.machine1.Manager", - assume_defaults = true, default_service = "org.freedesktop.machine1", default_path = "/org/freedesktop/machine1" )] @@ -46,29 +48,10 @@ pub trait Manager { /// CopyFromMachine method fn copy_from_machine(&self, name: &str, source: &str, destination: &str) -> zbus::Result<()>; - /// CopyFromMachineWithFlags method - fn copy_from_machine_with_flags( - &self, - name: &str, - source: &str, - destination: &str, - flags: u64, - ) -> zbus::Result<()>; - /// CopyToMachine method fn copy_to_machine(&self, name: &str, source: &str, destination: &str) -> zbus::Result<()>; - /// CopyToMachineWithFlags method - fn copy_to_machine_with_flags( - &self, - name: &str, - source: &str, - destination: &str, - flags: u64, - ) -> zbus::Result<()>; - /// CreateMachine method - #[allow(clippy::too_many_arguments)] fn create_machine( &self, name: &str, @@ -77,11 +60,10 @@ pub trait Manager { class: &str, leader: u32, root_directory: &str, - scope_properties: &[&(&str, &zbus::zvariant::Value<'_>)], + scope_properties: &[(&str, zbus::zvariant::Value<'_>)], ) -> zbus::Result; /// CreateMachineWithNetwork method - #[allow(clippy::too_many_arguments)] fn create_machine_with_network( &self, name: &str, @@ -91,7 +73,7 @@ pub trait Manager { leader: u32, root_directory: &str, ifindices: &[i32], - scope_properties: &[&(&str, &zbus::zvariant::Value<'_>)], + scope_properties: &[(&str, zbus::zvariant::Value<'_>)], ) -> zbus::Result; /// GetImage method @@ -101,7 +83,6 @@ pub trait Manager { fn get_image_hostname(&self, name: &str) -> zbus::Result; /// GetImageMachineID method - #[zbus(name = "GetImageMachineID")] fn get_image_machine_id(&self, name: &str) -> zbus::Result>; /// GetImageMachineInfo method @@ -111,7 +92,6 @@ pub trait Manager { ) -> zbus::Result>; /// GetImageOSRelease method - #[zbus(name = "GetImageOSRelease")] fn get_image_osrelease( &self, name: &str, @@ -124,22 +104,15 @@ pub trait Manager { fn get_machine_addresses(&self, name: &str) -> zbus::Result)>>; /// GetMachineByPID method - #[zbus(name = "GetMachineByPID")] fn get_machine_by_pid(&self, pid: u32) -> zbus::Result; /// GetMachineOSRelease method - #[zbus(name = "GetMachineOSRelease")] fn get_machine_osrelease( &self, name: &str, ) -> zbus::Result>; - /// GetMachineSSHInfo method - #[zbus(name = "GetMachineSSHInfo")] - fn get_machine_sshinfo(&self, name: &str) -> zbus::Result<(String, String)>; - /// GetMachineUIDShift method - #[zbus(name = "GetMachineUIDShift")] fn get_machine_uidshift(&self, name: &str) -> zbus::Result; /// KillMachine method @@ -190,14 +163,12 @@ pub trait Manager { fn open_machine_login(&self, name: &str) -> zbus::Result<(zbus::zvariant::OwnedFd, String)>; /// OpenMachinePTY method - #[zbus(name = "OpenMachinePTY")] fn open_machine_pty(&self, name: &str) -> zbus::Result<(zbus::zvariant::OwnedFd, String)>; /// OpenMachineRootDirectory method fn open_machine_root_directory(&self, name: &str) -> zbus::Result; /// OpenMachineShell method - #[allow(clippy::too_many_arguments)] fn open_machine_shell( &self, name: &str, @@ -208,7 +179,6 @@ pub trait Manager { ) -> zbus::Result<(zbus::zvariant::OwnedFd, String)>; /// RegisterMachine method - #[allow(clippy::too_many_arguments)] fn register_machine( &self, name: &str, @@ -220,7 +190,6 @@ pub trait Manager { ) -> zbus::Result; /// RegisterMachineWithNetwork method - #[allow(clippy::too_many_arguments)] fn register_machine_with_network( &self, name: &str, diff --git a/src/dbus_machine1_machine.rs b/src/dbus_machine1_machine.rs index abf9757..1607000 100644 --- a/src/dbus_machine1_machine.rs +++ b/src/dbus_machine1_machine.rs @@ -1,28 +1,28 @@ -//! # D-Bus interface proxy for: `org.freedesktop.machine1.Machine` +//! # DBus interface proxy for: `org.freedesktop.machine1.Machine` //! -//! This code was generated by `zbus-xmlgen` `5.0.1` from D-Bus introspection data. +//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. //! Source: `org.freedesktop.machine1-machine.xml`. //! //! You may prefer to adapt it, instead of using it verbatim. //! -//! More information can be found in the [Writing a client proxy] section of the zbus -//! documentation. +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. //! -//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the -//! following zbus API can be used: +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: //! //! * [`zbus::fdo::PeerProxy`] //! * [`zbus::fdo::IntrospectableProxy`] //! * [`zbus::fdo::PropertiesProxy`] //! -//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. -//! -//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html -//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. + use zbus::proxy; + #[proxy( interface = "org.freedesktop.machine1.Machine", - assume_defaults = true, default_service = "org.freedesktop.machine1" )] pub trait Machine { @@ -38,29 +38,16 @@ pub trait Machine { /// CopyFrom method fn copy_from(&self, source: &str, destination: &str) -> zbus::Result<()>; - /// CopyFromWithFlags method - fn copy_from_with_flags(&self, source: &str, destination: &str, flags: u64) - -> zbus::Result<()>; - /// CopyTo method fn copy_to(&self, source: &str, destination: &str) -> zbus::Result<()>; - /// CopyToWithFlags method - fn copy_to_with_flags(&self, source: &str, destination: &str, flags: u64) -> zbus::Result<()>; - /// GetAddresses method fn get_addresses(&self) -> zbus::Result)>>; /// GetOSRelease method - #[zbus(name = "GetOSRelease")] fn get_osrelease(&self) -> zbus::Result>; - /// GetSSHInfo method - #[zbus(name = "GetSSHInfo")] - fn get_sshinfo(&self) -> zbus::Result<(String, String)>; - /// GetUIDShift method - #[zbus(name = "GetUIDShift")] fn get_uidshift(&self) -> zbus::Result; /// Kill method @@ -70,7 +57,6 @@ pub trait Machine { fn open_login(&self) -> zbus::Result<(zbus::zvariant::OwnedFd, String)>; /// OpenPTY method - #[zbus(name = "OpenPTY")] fn open_pty(&self) -> zbus::Result<(zbus::zvariant::OwnedFd, String)>; /// OpenRootDirectory method @@ -112,14 +98,6 @@ pub trait Machine { #[zbus(property)] fn root_directory(&self) -> zbus::Result; - /// SSHAddress property - #[zbus(property, name = "SSHAddress")] - fn sshaddress(&self) -> zbus::Result; - - /// SSHPrivateKeyPath property - #[zbus(property, name = "SSHPrivateKeyPath")] - fn sshprivate_key_path(&self) -> zbus::Result; - /// Service property #[zbus(property)] fn service(&self) -> zbus::Result; @@ -139,8 +117,4 @@ pub trait Machine { /// Unit property #[zbus(property)] fn unit(&self) -> zbus::Result; - - /// VSockCID property - #[zbus(property, name = "VSockCID")] - fn vsock_cid(&self) -> zbus::Result; } diff --git a/cli/src/actions/diagnose.rs b/src/diagnose.rs similarity index 91% rename from cli/src/actions/diagnose.rs rename to src/diagnose.rs index f84c32b..32832a4 100644 --- a/cli/src/actions/diagnose.rs +++ b/src/diagnose.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, bail, Result}; use console::style; +use fs3::statvfs; use indicatif::HumanBytes; -use nix::sys::statvfs::statvfs; use std::env; use std::sync::mpsc::channel; use std::{fs::File, io::BufRead, time::Duration}; @@ -122,15 +122,15 @@ fn test_disk_io() -> Result { } fn test_disk_space() -> Result { - let stats = statvfs(&std::fs::canonicalize(".")?)?; - if stats.blocks_available() * stats.fragment_size() < (10 * 1024 * 1024 * 1024) { + let stats = statvfs(std::fs::canonicalize(".")?)?; + if stats.available_space() < (10 * 1024 * 1024 * 1024) { // 10 GB - Err(anyhow!("Disk space insufficient. Need at least 10 GB of free space to do something meaningful (You have {}).", HumanBytes(stats.blocks_available()*stats.fragment_size()))) + Err(anyhow!("Disk space insufficient. Need at least 10 GB of free space to do something meaningful (You have {}).", HumanBytes(stats.available_space()))) } else { Ok(format!( "Disk space is sufficient ({} free of {}).", - HumanBytes(stats.blocks_available() * stats.fragment_size()), - HumanBytes(stats.blocks() * stats.fragment_size()) + HumanBytes(stats.available_space()), + HumanBytes(stats.total_space()) )) } } @@ -163,7 +163,11 @@ pub fn run_diagnose() -> Result<()> { )); continue; } - lines.push(format!("{} {}", style("✓").green(), style(msg).green())) + lines.push(format!( + "{} {}", + style("✓").green(), + style(msg).green().bold() + )) } Err(err) => { has_error = true; diff --git a/src/fs/mod.rs b/src/fs/mod.rs deleted file mode 100644 index 78a9869..0000000 --- a/src/fs/mod.rs +++ /dev/null @@ -1,222 +0,0 @@ -use std::{ - ffi::OsString, - fs, - path::{self, Path, PathBuf}, - sync::Arc, -}; - -use crate::Result; - -pub mod overlayfs; -pub use overlayfs::OverlayFS; -pub mod tmpfs; - -/// A single layer in a layered filesystem. -pub trait Layer { - /// Returns the filesystem type of the layer, e.g. "overlay". - /// - /// This name should be the same as the fs_type listed in the /proc/<>/mountinfo file. - /// - /// For simple directory layers, this returns [None]. - fn fs_type(&self) -> Option<&'static str>; - - /// Returns the target directory to mount on. - fn target(&self) -> &Path; - - /// Returns whether the layer filesystem is mounted. - /// - /// For simple directory layers, this indicates if the directory exists. - fn is_mounted(&self) -> Result { - if let Some(ty) = self.fs_type() { - is_mounted(self.target(), ty) - } else { - unreachable!() - } - } - - /// Mounts the target layer filesystem. - /// - /// If the filesystem is already mounted, nothing is executed. - fn mount(&self) -> Result<()>; - - /// Un-mounts the target layer filesystem. - /// - /// If the filesystem is not mounted, nothing is executed. - fn unmount(&self) -> Result<()>; - - /// Reset the layer into the initial state. - /// - /// This can be invoked when the layer is either mounted or not. - /// The filesystem will be in un-mounted state after resetting. - /// - /// Warning: resetting the base system layer of workspaces will remove the base system, - /// leaving a base-system-unloaded workspace. - fn reset(&self) -> Result<()>; -} - -pub type BoxedLayer = Arc>; - -/// A overlay manager which composes a filesystem with multiple layers. -pub trait OverlayManager { - /// Returns the name of the layer manager, e.g. "overlay". - /// - /// This name should be the same as the fs_type listed in the /proc/<>/mountinfo file. - fn fs_type(&self) -> &'static str; - - /// Returns the target directory to mount on. - fn target(&self) -> &Path; - - /// Returns the upper layer of the layered filesystem, where changes - /// to the target directory will be reflected in. - fn upper_layer(&self) -> &BoxedLayer; - - /// Returns the lower layers to use. - fn lower_layers(&self) -> Vec<&BoxedLayer>; - - /// Returns whether the filesystem is mounted. - fn is_mounted(&self) -> Result { - is_mounted(self.target(), self.fs_type()) - } - - /// Mounts the target filesystem. - /// - /// If the filesystem is already mounted, nothing is executed. - fn mount(&self) -> Result<()>; - - /// Un-mounts the target filesystem. - /// - /// If the filesystem is not mounted, nothing is executed. - fn unmount(&self) -> Result<()>; - - /// Discard changes to the target filesystem. - /// - /// If the filesystem is mounted, it will be un-mounted. - fn rollback(&self) -> Result<()>; - - /// Commit changes in the upper layer to the toppest lower layer. - /// - /// If the filesystem is mounted, it will be un-mounted. - fn commit(&self) -> Result<()>; -} - -/// Checks if a path is a mountpoint with corresponding filesystem type. -pub(crate) fn is_mounted(mountpoint: &Path, fs_type: &str) -> Result { - let mountpoint = path::absolute(mountpoint)?; - let fs_type = OsString::from(fs_type); - let mountinfo_content: Vec = fs::read("/proc/self/mountinfo")?; - let parser = libmount::mountinfo::Parser::new(&mountinfo_content); - - for mount in parser { - let mount = mount?; - if mount.mount_point == mountpoint && mount.fstype == fs_type { - return Ok(true); - } - } - Ok(false) -} - -/// A simple layer which is backed by a directory. -#[derive(Debug, Clone, PartialEq)] -pub struct SimpleLayer(PathBuf); - -impl SimpleLayer { - /// Creates a new simple layer with the given path. - pub fn new>(path: P) -> Self { - Self(path.as_ref().to_owned()) - } -} - -impl> From

for SimpleLayer { - fn from(value: P) -> Self { - Self::new(value.as_ref()) - } -} - -impl Layer for SimpleLayer { - fn fs_type(&self) -> Option<&'static str> { - None - } - - fn target(&self) -> &Path { - &self.0 - } - - fn is_mounted(&self) -> Result { - Ok(self.target().exists()) - } - - fn mount(&self) -> Result<()> { - fs::create_dir_all(self.target())?; - Ok(()) - } - - fn unmount(&self) -> Result<()> { - Ok(()) - } - - fn reset(&self) -> Result<()> { - if self.target().exists() { - fs::remove_dir_all(self.target())?; - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use std::fs; - - use libmount::Tmpfs; - use nix::mount::{MntFlags, umount2}; - - use crate::{ - fs::{Layer, is_mounted}, - test::{TestDir, is_root}, - }; - - use super::SimpleLayer; - - #[test] - fn test_is_mounted() { - let testdir = TestDir::new(); - assert!(!is_mounted(testdir.path(), "tmpfs").unwrap()); - assert!(!is_mounted(testdir.path(), "overlay").unwrap()); - if is_root() { - Tmpfs::new(testdir.path()) - .size_bytes(1024 * 1024 * 4) - .mount() - .unwrap(); - assert!(is_mounted(testdir.path(), "tmpfs").unwrap()); - assert!(!is_mounted(testdir.path(), "overlay").unwrap()); - umount2(testdir.path(), MntFlags::MNT_DETACH).unwrap(); - assert!(!is_mounted(testdir.path(), "tmpfs").unwrap()); - } - } - - #[test] - fn test_simple_layer() { - let testdir = TestDir::new(); - let dir = testdir.path().join("layer"); - let layer = SimpleLayer::new(&dir); - - assert!(!dir.exists()); - assert_eq!(layer.fs_type(), None); - assert!(!layer.is_mounted().unwrap()); - - layer.mount().unwrap(); - // behaviour compatible with Ciel <= 3.6.0 - assert!(matches!(layer.mount(), Ok(()))); - assert!(layer.is_mounted().unwrap()); - assert!(dir.exists()); - - fs::write(dir.join("Test"), "Test").unwrap(); - assert_eq!(fs::read_to_string(dir.join("Test")).unwrap(), "Test"); - - layer.unmount().unwrap(); - assert_eq!(fs::read_to_string(dir.join("Test")).unwrap(), "Test"); - assert!(matches!(layer.unmount(), Ok(()))); - - layer.reset().unwrap(); - assert!(!dir.join("Test").exists()); - } -} diff --git a/src/fs/overlayfs.rs b/src/fs/overlayfs.rs deleted file mode 100644 index e545d96..0000000 --- a/src/fs/overlayfs.rs +++ /dev/null @@ -1,432 +0,0 @@ -use std::{ - ffi::OsStr, - fs::{self, File}, - io::{BufRead, BufReader}, - os::unix::{ - ffi::OsStrExt, - fs::{FileTypeExt, MetadataExt, PermissionsExt}, - }, - path::{Path, PathBuf}, - process::Command, - sync::Arc, -}; - -use libmount::Overlay; -use log::info; -use nix::mount::{umount2, MntFlags}; - -use crate::{Error, Result}; - -use super::{BoxedLayer, OverlayManager, SimpleLayer}; - -/// A `overlay` filesystem-backed overlay manager. -/// -/// In non-compat mode, The structure of the upper layer is as follows: -/// - `diff` (upper directory) -/// - `diff.tmp` (work directory) -/// -/// To keep compatibility with old containers created by Ciel <= 3.6.0, -/// a compatibile mode is supported, which can be enabled with [OverlayFS::new_compat]. -/// -/// In compatibile mode, the upper layer must be a simple layer, pointing to -/// the container directory, rather than `upper` subdirectory. -/// When `rollback` is called, OverlayFS in compat mode will not -/// really call the [super::Layer::reset], instead it removes the old directories. -pub struct OverlayFS { - target: PathBuf, - upper: BoxedLayer, - compat: bool, - lower: Vec, - volatile: bool, -} - -impl OverlayFS { - /// Creates a new OverlayFS manager. - pub fn new>( - target: P, - upper: BoxedLayer, - lower: Vec, - volatile: bool, - ) -> Self { - Self { - target: target.as_ref().to_owned(), - upper, - compat: false, - lower, - volatile, - } - } - - /// Creates a new OverlayFS manager which is compatible with old containers. - pub fn new_compat>( - target: P, - upper: P, - lower: Vec, - volatile: bool, - ) -> Self { - Self { - target: target.as_ref().to_owned(), - upper: Arc::new(Box::new(SimpleLayer::new(upper.as_ref()))), - compat: true, - lower, - volatile, - } - } -} - -impl OverlayManager for OverlayFS { - fn fs_type(&self) -> &'static str { - "overlay" - } - - fn target(&self) -> &Path { - &self.target - } - - fn upper_layer(&self) -> &BoxedLayer { - &self.upper - } - - fn lower_layers(&self) -> Vec<&BoxedLayer> { - self.lower.iter().collect() - } - - fn mount(&self) -> Result<()> { - if self.is_mounted()? { - return Ok(()); - } - if !self.upper.is_mounted()? { - self.upper.mount()?; - } - let mut lowerdirs = Vec::new(); - for lower in &self.lower { - if !lower.is_mounted()? { - lower.mount()?; - } - lowerdirs.push(lower.target()); - } - - let upperdir = self.upper.target().join("diff"); - let workdir = self.upper.target().join("diff.tmp"); - // these two directories may have been created by older versions of Ciel - if !upperdir.exists() { - fs::create_dir(&upperdir)?; - } - if !workdir.exists() { - fs::create_dir(&workdir)?; - } - - ensure_overlayfs_support()?; - if !self.target.exists() { - fs::create_dir(&self.target)?; - } - let mut overlay = Overlay::writable( - lowerdirs.iter().map(|x| x.as_ref()), - upperdir.clone(), - workdir.clone(), - &self.target, - ); - if self.volatile { - overlay.set_options(b"volatile".to_vec()); - } - - if workdir.join("work/incompat").exists() { - return Err(Error::OverlayFSIncompat(workdir)); - } - - info!("overlayfs: mounting at {:?}", self.target); - overlay.mount()?; - Ok(()) - } - - fn unmount(&self) -> Result<()> { - if !self.is_mounted()? { - return Ok(()); - } - info!("overlayfs: un-mounting at {:?}", self.target); - umount2(&self.target, MntFlags::MNT_DETACH)?; - fs::remove_dir_all(&self.target)?; - self.upper.unmount()?; - for lower in &self.lower { - lower.unmount()?; - } - Ok(()) - } - - fn rollback(&self) -> Result<()> { - self.unmount()?; - if self.compat { - fs::remove_dir_all(self.upper.target().join("diff"))?; - fs::remove_dir_all(self.upper.target().join("diff.tmp"))?; - } else { - self.upper.reset()?; - } - // avoid resetting the base system layer - if let Some((_, lowers)) = &self.lower.split_last() { - for lower in lowers.iter() { - lower.reset()?; - } - } - Ok(()) - } - - fn commit(&self) -> Result<()> { - info!("overlayfs: commiting changes in {:?}", self.target); - if self.volatile { - // for safety reasons - nix::unistd::sync(); - } - - let upper = self.upper.target().join("diff"); - let lower = self.lower.last().unwrap().target(); - let diffs = self.diff()?; - - // FIXME: use extract_if in the future - // first, perform all the deletion actions - for i in diffs.iter() { - match i { - Diff::WhiteoutFile(_) => patch_lower(i, &upper, lower)?, - _ => continue, - } - } - // second, apply other things - for i in diffs.iter() { - match i { - Diff::WhiteoutFile(_) => continue, - _ => patch_lower(i, &upper, lower)?, - } - } - - // clear all the remaining items in the upper layer - self.rollback()?; - - Ok(()) - } -} - -/// OverlayFS operations -#[derive(Debug)] -enum Diff { - Symlink(PathBuf), - OverrideDir(PathBuf), - RenamedDir(PathBuf, PathBuf), - NewDir(PathBuf), - ModifiedDir(PathBuf), // Modify permission only - WhiteoutFile(PathBuf), // Dir or File - File(PathBuf), // Simple modified or new file -} - -impl OverlayFS { - fn diff(&self) -> Result> { - let mut diffs: Vec = Vec::new(); - let mut processed_dirs: Vec = Vec::new(); - - let upper = self.upper.target().join("diff"); - let lower = self.lower.last().unwrap().target(); - - // skip the root entry - for entry in walkdir::WalkDir::new(&upper).into_iter().skip(1) { - let path: PathBuf = entry?.path().to_path_buf(); - let rel_path = path.strip_prefix(&upper)?.to_path_buf(); - let lower_path = lower.join(&rel_path).to_path_buf(); - - if processed_dirs - .iter() - .any(|prefix| rel_path.strip_prefix(prefix).is_ok()) - { - continue; // We already dealt with it - } - - let meta = fs::symlink_metadata(&path)?; - let file_type = meta.file_type(); - if file_type.is_symlink() { - // Just move the symlink - diffs.push(Diff::Symlink(rel_path.clone())); - } else if meta.is_dir() { - // Deal with dirs - let metacopy = xattr::get(&path, "trusted.overlay.metacopy")?; - if let Some(_data) = metacopy { - return Err(Error::MetaCopyUnsupported); - } - - let opaque = xattr::get(&path, "trusted.overlay.opaque")?; - if let Some(text) = opaque { - // the new dir (completely) replace the old one - if text == b"y" { - // Delete corresponding dir - diffs.push(Diff::OverrideDir(rel_path.clone())); - processed_dirs.push(rel_path.clone()); - continue; - } - } - - let redirect = xattr::get(&path, "trusted.overlay.redirect")?; - if let Some(from_utf8) = redirect { - // Renamed - let mut from_rel_path = PathBuf::from(OsStr::from_bytes(&from_utf8)); - if from_rel_path.is_absolute() { - // abs path from root of OverlayFS - from_rel_path = from_rel_path.strip_prefix("/")?.to_path_buf(); - } else { - // rel path, same parent dir as the origin - let mut from_path = path.clone(); - from_path.pop(); - from_path.push(PathBuf::from(&from_rel_path)); - from_rel_path = from_path.strip_prefix(&upper)?.to_path_buf(); - } - diffs.push(Diff::RenamedDir(from_rel_path, rel_path)); - continue; - } - if !lower_path.is_dir() { - // New dir - diffs.push(Diff::NewDir(rel_path.clone())); - } else { - // Modified - diffs.push(Diff::ModifiedDir(rel_path.clone())); - } - } else { - // Deal with files - if file_type.is_char_device() && meta.rdev() == 0 { - // Whiteout file! - diffs.push(Diff::WhiteoutFile(rel_path.clone())); - } else if lower_path.is_dir() { - // A new file overrides an old directory - diffs.push(Diff::OverrideDir(rel_path.clone())); - } else { - diffs.push(Diff::File(rel_path.clone())); - } - } - } - - Ok(diffs) - } -} - -fn ensure_overlayfs_support() -> Result<()> { - let f = File::open("/proc/filesystems")?; - let reader = BufReader::new(f); - for line in reader.lines() { - let line = line?; - let mut fs_type = line.splitn(2, '\t'); - if fs_type.nth(1) == Some("overlay") { - return Ok(()); - } - } - - Command::new("modprobe") - .arg("overlay") - .status() - .map_err(|_| Error::OverlayFSUnavailable)?; - - Ok(()) -} - -fn rename_file(from: &Path, to: &Path) -> Result<()> { - if to.symlink_metadata().is_ok() { - if to.is_dir() { - fs::remove_dir_all(to)?; - } else { - fs::remove_file(to)?; - } - } - - match fs::rename(from, to) { - Ok(_) => return Ok(()), - Err(err) => { - // FIXME: use CrossesDevices when stablized - // now we just fallthrough - _ = err; - // if err.kind() != std::io::ErrorKind::CrossesDevices { - // return Err(err.into()); - // } - } - } - - let from_meta = from.symlink_metadata()?; - if from_meta.is_symlink() { - std::os::unix::fs::symlink(fs::read_link(from)?, to)?; - fs::remove_file(from)?; - } else if from_meta.is_file() { - fs::copy(from, to)?; - fs::remove_file(from)?; - } else if from_meta.is_dir() { - fs::create_dir_all(to)?; - fs::set_permissions(to, from.metadata()?.permissions())?; - for entry in fs::read_dir(from)? { - let entry = entry?; - rename_file(&from.join(entry.file_name()), &to.join(entry.file_name()))?; - } - fs::remove_dir_all(from)?; - } else { - unreachable!(); - } - Ok(()) -} - -fn patch_lower(action: &Diff, upper: &Path, lower: &Path) -> Result<()> { - match action { - Diff::Symlink(path) => { - let upper_path = upper.join(path); - let lower_path = lower.join(path); - // Replace lower dir with upper - rename_file(&upper_path, &lower_path)?; - } - Diff::OverrideDir(path) => { - let upper_path = upper.join(path); - let lower_path = lower.join(path); - // Replace lower dir with upper - if lower_path.is_dir() { - // If exists and was not removed already, then remove it - fs::remove_dir_all(&lower_path)?; - } else if lower_path.is_file() { - // If it's a file, then remove it as well - fs::remove_file(&lower_path)?; - } - rename_file(&upper_path, &lower_path)?; - } - Diff::RenamedDir(from, to) => { - // TODO: Implement copy down - // Such dir will include diff files, so this - // section need more testing - let from_path = lower.join(from); - let to_path = lower.join(to); - // TODO: Merge files from upper to lower - // Replace lower dir with upper - rename_file(&from_path, &to_path)?; - } - Diff::NewDir(path) => { - let lower_path = lower.join(path); - // Construct lower path - fs::create_dir_all(lower_path)?; - } - Diff::ModifiedDir(path) => { - // Do nothing, just sync permission - let upper_path = upper.join(path); - let lower_path = lower.join(path); - let upper_meta = fs::metadata(upper_path)?; - let lower_meta = fs::metadata(lower_path)?; - - if upper_meta.mode() != lower_meta.mode() { - lower_meta.permissions().set_mode(lower_meta.mode()); - } - } - Diff::WhiteoutFile(path) => { - let lower_path = lower.join(path); - if lower_path.is_dir() { - fs::remove_dir_all(&lower_path)?; - } else if lower_path.is_file() { - fs::remove_file(&lower_path)?; - } - // remove the whiteout in the upper layer - fs::remove_file(upper.join(path))?; - } - Diff::File(path) => { - let upper_path = upper.join(path); - let lower_path = lower.join(path); - // Move upper file to overwrite the lower - rename_file(&upper_path, &lower_path)?; - } - } - - Ok(()) -} diff --git a/src/fs/tmpfs.rs b/src/fs/tmpfs.rs deleted file mode 100644 index afb2176..0000000 --- a/src/fs/tmpfs.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; - -use libmount::Tmpfs; -use log::info; -use nix::mount::{MntFlags, umount2}; - -use crate::{Result, instance::TmpfsConfig}; - -use super::Layer; - -/// A `tmpfs`-backed filesystem layer. -pub struct TmpfsLayer { - target: PathBuf, - size: usize, -} - -impl TmpfsLayer { - pub fn new>(target: P, config: &TmpfsConfig) -> Self { - Self { - target: target.as_ref().into(), - size: config.size_bytes(), - } - } -} - -impl Layer for TmpfsLayer { - fn fs_type(&self) -> Option<&'static str> { - Some("tmpfs") - } - - fn target(&self) -> &Path { - &self.target - } - - fn mount(&self) -> Result<()> { - info!("tmpfs: mounting at {:?}", self.target); - if !self.target.exists() { - fs::create_dir_all(&self.target)?; - } - Tmpfs::new(&self.target).size_bytes(self.size).mount()?; - Ok(()) - } - - fn unmount(&self) -> Result<()> { - // tmpfs ignores unmount to avoid data loss - Ok(()) - } - - fn reset(&self) -> Result<()> { - if !self.is_mounted()? { - return Ok(()); - } - info!("tmpfs: un-mounting at {:?}", self.target); - umount2(&self.target, MntFlags::MNT_DETACH)?; - fs::remove_dir_all(&self.target)?; - Ok(()) - } -} - -#[cfg(test)] -mod test { - use crate::{ - fs::Layer, - instance::TmpfsConfig, - test::{TestDir, is_root}, - }; - - use super::TmpfsLayer; - - #[test] - fn test_tmpfs() { - let testdir = TestDir::new(); - let layer = TmpfsLayer::new(testdir.path(), &TmpfsConfig::default()); - assert!(!layer.is_mounted().unwrap()); - if !is_root() { - return; - } - layer.mount().unwrap(); - assert!(layer.is_mounted().unwrap()); - layer.unmount().unwrap(); - assert!(layer.is_mounted().unwrap()); - layer.reset().unwrap(); - assert!(!layer.is_mounted().unwrap()); - } -} diff --git a/src/instance.rs b/src/instance.rs deleted file mode 100644 index 83c7f82..0000000 --- a/src/instance.rs +++ /dev/null @@ -1,309 +0,0 @@ -use std::{ - fmt::Debug, - fs, - path::{Path, PathBuf}, - sync::{Arc, RwLock}, -}; - -use log::info; -use serde::{Deserialize, Serialize}; - -use crate::{workspace::Workspace, Container, Error, Result}; - -/// A Ciel instance. -/// -/// Each instance maps to a build container. To begin interaction with -/// the container, use [Instance::open], which returns a [Container] and -/// locks the container to avoid asynchronized operations. -#[derive(Clone)] -pub struct Instance { - workspace: Workspace, - name: Arc, - path: Arc, - config: Arc>, -} - -impl Instance { - pub(crate) fn new(workspace: Workspace, name: String) -> Result { - let path = workspace - .directory() - .join(Workspace::INSTANCES_DIR) - .join(&name); - - if !path.is_dir() { - return Err(Error::InstanceNotFound(name)); - } - - // Instance-level config.toml is not created by Ciel <= 3.6.0. - // So fallback to default configuration for these. - let config_path = path.join(InstanceConfig::PATH); - let config = if !config_path.exists() { - fs::write( - path.join(InstanceConfig::PATH), - InstanceConfig::default().serialize()?, - )?; - InstanceConfig::default() - } else { - InstanceConfig::load(config_path)? - }; - - Ok(Self { - workspace, - name: name.into(), - path: path.into(), - config: Arc::new(config.into()), - }) - } - - /// Returns the workspace including this instance. - pub fn workspace(&self) -> &Workspace { - &self.workspace - } - - /// Returns the name of this instance. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the instance directory. - pub fn directory(&self) -> &Path { - &self.path - } - - /// Gets the instance configuration. - pub fn config(&self) -> InstanceConfig { - self.config.read().unwrap().to_owned() - } - - /// Modifies the instance configuration. - pub fn set_config(&self, config: InstanceConfig) -> Result<()> { - fs::write( - self.directory().join(InstanceConfig::PATH), - config.serialize()?, - )?; - *self.config.write()? = config; - Ok(()) - } - - /// Opens the build container for further operations. - /// - /// This is equivalent to calling [Container::open]. - pub fn open(&self) -> Result { - Container::open(self.to_owned()) - } - - /// Destories the container, removing all related files. - pub fn destroy(self) -> Result<()> { - let container = self.open()?; - // some layers, such as tmpfs, requires rollback to fully un-mount - container.rollback()?; - info!("{}: destroying", self.name); - fs::remove_dir_all(self.directory())?; - Ok(()) - } -} - -impl Debug for Instance { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "CIEL instance `{}` @ {:?}", - self.name(), - self.workspace.directory(), - )) - } -} - -impl From for PathBuf { - fn from(value: Instance) -> Self { - value.directory().to_owned() - } -} - -impl PartialEq for Instance { - fn eq(&self, other: &Self) -> bool { - self.path == other.path - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct InstanceConfig { - version: usize, - /// Extra APT repositories - #[serde(default, alias = "extra-repos")] - pub extra_apt_repos: Vec, - /// Extra systemd-nspawn options - #[serde(default, alias = "nspawn-options")] - pub extra_nspawn_options: Vec, - /// Whether local repository (the output directory) should be enabled in the container. - #[serde(default)] - pub use_local_repo: bool, - /// tmpfs settings. - /// - /// Set to `None` to disable tmpfs for filesystem. - #[serde(default)] - pub tmpfs: Option, - /// Whether TREE should be mounted as read-only. - #[serde(default)] - pub readonly_tree: bool, - /// Path to OUTPUT directory. - #[serde(default)] - pub output: Option, -} - -impl Default for InstanceConfig { - fn default() -> Self { - Self { - version: Self::CURRENT_VERSION, - extra_apt_repos: vec![], - extra_nspawn_options: vec![], - use_local_repo: true, - tmpfs: None, - readonly_tree: false, - output: None, - } - } -} - -impl InstanceConfig { - /// The default path for instance configuration. - pub const PATH: &str = "config.toml"; - - /// The current version of instance configuration format. - pub const CURRENT_VERSION: usize = 3; - - /// Loads a instance configuration from a given file path. - pub fn load>(path: P) -> Result { - let path = path.as_ref().to_path_buf(); - if path.exists() { - fs::read_to_string(&path)?.as_str().try_into() - } else { - Err(Error::ConfigNotFound(path)) - } - } - - /// Deserializes a instance configuration TOML. - pub fn parse(config: &str) -> Result { - let config = toml::from_str::(config)?; - Ok(config) - } - - /// Serializes a instance configuration into TOML. - pub fn serialize(&self) -> Result { - Ok(toml::to_string_pretty(&self)?) - } -} - -impl TryFrom<&str> for InstanceConfig { - type Error = crate::Error; - - fn try_from(value: &str) -> std::result::Result { - Self::parse(value) - } -} - -impl TryFrom<&InstanceConfig> for String { - type Error = crate::Error; - - fn try_from(value: &InstanceConfig) -> std::result::Result { - value.serialize() - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -#[derive(Default)] -pub struct TmpfsConfig { - #[serde(default)] - pub size: Option, -} - -impl TmpfsConfig { - /// Returns the size of tmpfs or the default value (4 GiB), in MiB. - pub fn size_or_default(&self) -> usize { - self.size.unwrap_or(4096) - } - - /// Returns the size of tmpfs or the default value, in bytes - pub fn size_bytes(&self) -> usize { - self.size_or_default() * 1024 * 1024 - } -} - -#[cfg(test)] -mod test { - use crate::{test::TestDir, Error}; - use test_log::test; - - use super::InstanceConfig; - - #[test] - fn test_instance_config() { - let config = InstanceConfig::default(); - let serialized = config.serialize().unwrap(); - assert_eq!( - serialized, - r##"version = 3 -extra-apt-repos = [] -extra-nspawn-options = [] -use-local-repo = true -readonly-tree = false -"## - ); - assert_eq!(InstanceConfig::parse(&serialized).unwrap(), config); - } - - #[test] - fn test_instance() { - let testdir = TestDir::from("testdata/simple-workspace"); - let workspace = testdir.workspace().unwrap(); - dbg!(&workspace); - - let instance = workspace.instance("test").unwrap(); - dbg!(&instance); - assert_eq!(instance.workspace(), &workspace); - assert_eq!(instance.name(), "test"); - assert_eq!( - instance.directory(), - testdir.path().join(".ciel/container/instances/test") - ); - - assert!(matches!( - workspace.instance("a"), - Err(Error::InstanceNotFound(_)) - )); - } - - #[test] - fn test_instance_migration() { - let testdir = TestDir::from("testdata/old-workspace"); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - assert!(ws.is_system_loaded()); - let inst = ws.instance("test").unwrap(); - dbg!(&inst); - } - - #[test] - fn test_instance_destroy() { - let testdir = TestDir::from("testdata/simple-workspace"); - let workspace = testdir.workspace().unwrap(); - dbg!(&workspace); - assert_eq!( - workspace - .instances() - .unwrap() - .iter() - .map(|i| i.name().to_owned()) - .collect::>(), - vec!["test".to_string(), "tmpfs".to_string()] - ); - let instance = workspace.instance("test").unwrap(); - dbg!(&instance); - instance.destroy().unwrap(); - assert!(matches!( - workspace.instance("test"), - Err(Error::InstanceNotFound(_)) - )); - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 9170ae5..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Ciel (/sjɛl/) 3 is an integrated packaging environment for AOSC OS. -//! -//! Ciel uses `systemd-nspawn` as container backend and `overlay` file system -//! for layered filesystem. - -pub mod build; -pub mod container; -mod dbus_machine1_machine; -mod dbus_machine1_manager; -pub mod fs; -pub mod instance; -pub mod machine; -pub mod repo; -pub mod workspace; - -pub use container::{Container, ContainerConfig, ContainerState}; -pub use instance::{Instance, InstanceConfig}; -pub use machine::{Machine, MachineState}; -pub use repo::SimpleAptRepository; -pub use workspace::{Workspace, WorkspaceConfig}; - -use std::ffi::OsString; - -pub type Result = std::result::Result; - -/// An error produced by Ciel. -#[non_exhaustive] -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("I/O error: {0}")] - IoError(#[from] std::io::Error), - #[error("Some Mutex/RwLock are poisoned")] - PoisonError, - #[error("Unable to parse mountinfo file: {0}")] - MountInfoParseError(#[from] libmount::mountinfo::ParseError), - #[error("Mount error: {0}")] - MountError(String), - #[error(transparent)] - SyscallError(#[from] nix::Error), - #[error(transparent)] - FSTraverseError(#[from] walkdir::Error), - #[error(transparent)] - StripPrefixError(#[from] std::path::StripPrefixError), - #[error("D-Bus error: {0}")] - DBusError(#[from] zbus::Error), - #[error("libgit2 error: {0}")] - GitError(#[from] git2::Error), - #[error("Time formatting error: {0}")] - TimeFormatError(#[from] time::error::Format), - - #[error("Configuration file is not found at {0}")] - ConfigNotFound(std::path::PathBuf), - #[error("Invalid TOML: {0}")] - InvalidToml(#[from] toml::de::Error), - #[error("Unable to serialize into TOML: {0}")] - TomlSerializerError(#[from] toml::ser::Error), - #[error("Invalid maintainer information")] - InvalidMaintainerInfo, - #[error("Maintainer name is required")] - MaintainerNameNeeded, - - #[error("Not a Ciel workspace (.ciel directory does not exist)")] - NotAWorkspace, - #[error("A Ciel workspace is already initialized")] - WorkspaceAlreadyExists, - #[error("Ciel workspace is broken")] - BrokenWorkspace, - #[error("Unsupported workspace version: got {0}")] - UnsupportedWorkspaceVersion(usize), - - #[error("Invalid instance name: {0:?}")] - InvalidInstanceName(OsString), - #[error("Instance not found: {0}")] - InstanceNotFound(String), - #[error("Invalid instance path: {0}")] - InvalidInstancePath(std::path::PathBuf), - #[error("Improper state")] - ImproperState, - #[error("Subcommand error: {0}")] - SubcommandError(std::process::ExitStatus), - #[error("Timeout booting machine")] - BootTimeout, - #[error("Timeout poweroff machine")] - PoweroffTimeout, - - #[error("Your kernel does not support overlayfs")] - OverlayFSUnavailable, - #[error("OverlayFS at {0} cannot be mounted due to incompat features")] - OverlayFSIncompat(std::path::PathBuf), - #[error("Ciel does not support overlayfs metacopy")] - MetaCopyUnsupported, - - #[error("Unable to scan deb file '{0}': {1}")] - DebScanError(std::path::PathBuf, repo::scan::ScanError), - - #[error("Invalid bincode: {0}")] - InvalidBincode(#[from] bincode::Error), - #[error("Nested package group exceeded 32 levels")] - NestedPackageGroup, -} - -impl From> for Error { - fn from(_: std::sync::PoisonError) -> Self { - Self::PoisonError - } -} - -impl From for Error { - fn from(err: libmount::Error) -> Self { - // discard details so that Error can be converted into anyhow::Error simply - Self::MountError(format!("{:?}", err)) - } -} - -#[cfg(test)] -pub(crate) mod test { - use std::{fs, path::Path}; - - use tempfile::TempDir; - - use crate::{ - Result, - repo::SimpleAptRepository, - workspace::{Workspace, WorkspaceConfig}, - }; - - pub fn is_root() -> bool { - nix::unistd::geteuid().is_root() - } - - #[derive(Debug)] - pub struct TestDir(TempDir); - - impl AsRef for TestDir { - fn as_ref(&self) -> &Path { - self.0.path() - } - } - - impl From for TestDir { - fn from(value: TempDir) -> Self { - Self(value) - } - } - - fn copy_file(from: &Path, to: &Path) { - if from.is_symlink() { - std::os::unix::fs::symlink(fs::read_link(from).unwrap(), to).unwrap(); - } else if from.is_file() { - fs::copy(from, to).unwrap(); - } else if from.is_dir() { - fs::create_dir_all(to).unwrap(); - fs::set_permissions(to, from.metadata().unwrap().permissions()).unwrap(); - for entry in fs::read_dir(from).unwrap() { - let entry = entry.unwrap(); - copy_file(&from.join(entry.file_name()), &to.join(entry.file_name())); - } - } else { - panic!("unsupported file type"); - } - } - - impl TestDir { - pub fn new() -> Self { - let dir = TempDir::with_prefix("ciel-").unwrap(); - println!("test data: {:?}", dir.path()); - dir.into() - } - - pub fn from(template: &str) -> Self { - let dir = Self::new(); - println!("copying test data: {} -> {:?}", template, dir.path()); - copy_file(Path::new(template), dir.path()); - dir - } - - pub fn path(&self) -> &Path { - self.0.path() - } - - pub fn workspace(&self) -> Result { - Workspace::new(self.path()) - } - - pub fn init_workspace(&self, config: WorkspaceConfig) -> Result { - Workspace::init(self.path(), config) - } - - pub fn apt_repo(&self) -> SimpleAptRepository { - SimpleAptRepository::new(self.path().join("debs")) - } - } -} diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..02a3870 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,32 @@ +#[macro_export] +macro_rules! info { + ($($arg:tt)+) => { + eprint!("{} ", style("info:").cyan().bold()); + eprintln!($($arg)+); + }; +} + +#[macro_export] +macro_rules! warn { + ($($arg:tt)+) => { + eprint!("{} ", style("warning:").yellow().bold()); + eprintln!($($arg)+); + }; +} + +#[macro_export] +macro_rules! error { + ($($arg:tt)+) => { + eprint!("{} ", style("error:").red().bold()); + eprintln!($($arg)+); + }; +} + +#[inline] +pub fn color_bool(pred: bool) -> &'static str { + if pred { + "\x1b[1m\x1b[32mYes\x1b[0m" + } else { + "\x1b[34mNo\x1b[0m" + } +} diff --git a/src/machine.rs b/src/machine.rs index 287934d..7d86823 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -1,356 +1,429 @@ +//! This module contains systemd machined related APIs + +use crate::common::{is_legacy_workspace, CIEL_INST_DIR}; +use crate::dbus_machine1::ManagerProxyBlocking; +use crate::dbus_machine1_machine::MachineProxyBlocking; +use crate::overlayfs::is_mounted; +use crate::{info, overlayfs::LayerManager, warn}; +use adler32::adler32; +use anyhow::{anyhow, Result}; +use console::style; +use libc::{c_char, ftok, waitpid, WNOHANG}; +use libsystemd_sys::bus::{sd_bus_flush_close_unref, sd_bus_open_system_machine}; use std::{ ffi::{CString, OsStr}, - fs, mem::MaybeUninit, - os::unix::ffi::OsStrExt, - path::{Path, PathBuf}, - process::{Child, Command, ExitStatus, Stdio}, - sync::Arc, - time::Duration, + process::Command, }; +use std::{fs, time::Duration}; +use std::{os::unix::ffi::OsStrExt, process::Child}; +use std::{path::Path, process::Stdio, thread::sleep}; +use zbus::blocking::Connection; -use log::{debug, info, warn}; - -use crate::{ - ContainerConfig, Error, Result, dbus_machine1_machine::MachineProxyBlocking, - dbus_machine1_manager::ManagerProxyBlocking, -}; +const DEFAULT_NSPAWN_OPTIONS: &[&str] = &[ + "-qb", + "--capability=CAP_IPC_LOCK", + "--system-call-filter=swapcontext", +]; -/// A systemd-nspawn machine. -pub struct Machine { - config: Arc, - rootfs_path: PathBuf, - dbus_conn: zbus::blocking::Connection, +/// Instance status information +#[derive(Debug)] +pub struct CielInstance { + name: String, + // namespace name (in the form of `$name-$id`) + pub ns_name: String, + pub mounted: bool, + running: bool, + pub started: bool, + booted: Option, } -impl Machine { - pub(crate) fn new>( - config: Arc, - rootfs_path: P, - ) -> Result { - Ok(Self { - config, - rootfs_path: rootfs_path.as_ref().to_owned(), - dbus_conn: zbus::blocking::Connection::system()?, - }) +/// Used for getting the instance name from Ciel 1/2 +fn legacy_container_name(path: &Path) -> Result { + let key_id; + let current_dir = std::env::current_dir()?; + let name = path + .file_name() + .ok_or_else(|| anyhow!("Invalid container path: {:?}", path))?; + let mut path = current_dir.as_os_str().as_bytes().to_owned(); + path.push(0); // add trailing null terminator + unsafe { + // unsafe because of the `ftok` invocation + key_id = ftok(path.as_ptr() as *const c_char, 0); } - - /// Returns the NS name of machine. - pub fn name(&self) -> &str { - &self.config.ns_name + if key_id < 0 { + return Err(anyhow!("ftok() failed.")); } - /// Returns the state of machine. - pub fn state(&self) -> Result { - let proxy = ManagerProxyBlocking::new(&self.dbus_conn)?; - let path = proxy.get_machine(self.name()); - if let Err(zbus::Error::MethodError(ref err_name, _, _)) = path { - if err_name.as_ref() == "org.freedesktop.machine1.NoSuchMachine" { - return Ok(MachineState::Down); - } - } - let path = path?; - let proxy = MachineProxyBlocking::builder(&self.dbus_conn) - .path(&path)? - .build()?; - let state = proxy.state()?; - // Sometimes the system in the container is misconfigured, - // so we also accept "degraded" status as "running" - if state != "running" && state != "degraded" { - return Ok(MachineState::Starting); - } + Ok(format!( + "{}-{:x}", + name.to_str() + .ok_or_else(|| anyhow!("Container name is not valid unicode."))?, + key_id + )) +} - // inspect the cmdline of the PID 1 in the container - let f = std::fs::read(format!("/proc/{}/cmdline", proxy.leader()?))?; - // take until the first null byte - let pos = f.iter().position(|c| *c == 0u8).unwrap(); - // ... well, of course it's a path - let path = Path::new(OsStr::from_bytes(&f[..pos])); - let exe_name = path.file_name(); - // if PID 1 is systemd or init (System V init) then it should be a "booted" container - if let Some(exe_name) = exe_name { - if exe_name == "systemd" || exe_name == "init" { - return Ok(MachineState::Running); - } +/// Used for getting the instance name from Ciel 3+ +fn new_container_name(path: &Path) -> Result { + // New container name is calculated using the following formula: + // $name-adler32($PWD) + let hash = adler32(path.as_os_str().as_bytes())?; + let name = path + .file_name() + .ok_or_else(|| anyhow!("Invalid container path: {:?}", path))?; + + Ok(format!( + "{}-{:x}", + name.to_str() + .ok_or_else(|| anyhow!("Container name is not valid unicode."))?, + hash + )) +} + +fn try_open_container_bus(ns_name: &str) -> Result<()> { + // There are bunch of trickeries happening here + // First we initialize an empty pointer + let mut buf = MaybeUninit::uninit(); + // Convert the ns_name to C-style `const char*` (NUL-terminated) + let ns_name = CString::new(ns_name)?; + // unsafe: these functions are from libsystemd, which involving FFI calls + unsafe { + // Try opening a connection to the container + if sd_bus_open_system_machine(buf.as_mut_ptr(), ns_name.as_ptr()) >= 0 { + // If successful, just close the connection and drop the pointer + sd_bus_flush_close_unref(buf.assume_init()); + return Ok(()); } - Ok(MachineState::Starting) } - /// Boots this machine up. - /// - /// Note that the container configuration is not yet applied after this. - pub fn boot(&self) -> Result<()> { - info!("{}: waiting for machine to start...", self.name()); - let mut child = Command::new("systemd-nspawn"); - child - .args([ - "-qb", - "--capability=CAP_IPC_LOCK", - "--system-call-filter=swapcontext", - ]) - .args(&self.config.workspace_config.extra_nspawn_options) - .args(&self.config.instance_config.extra_nspawn_options) - .args([ - "-D", - self.rootfs_path - .to_str() - .ok_or_else(|| Error::InvalidInstancePath(self.rootfs_path.to_owned()))?, - "-M", - self.name(), - "--", - ]) - .env("SYSTEMD_NSPAWN_TMPFS_TMP", "0") - .stdout(Stdio::null()) - .stderr(Stdio::null()); - debug!( - "invoking systemd-nspawn {:?}", - child.get_args().collect::>().join(OsStr::new(" ")) - ); - let child = child.spawn()?; - wait_for_machine(child, self.name())?; - Ok(()) + Err(anyhow!("Could not open container bus")) +} + +fn wait_for_container(child: &mut Child, ns_name: &str, retry: usize) -> Result<()> { + for i in 0..retry { + let exited = child.try_wait()?; + if let Some(status) = exited { + return Err(anyhow!("nspawn exited too early! (Status: {})", status)); + } + // why this is used: because PTY spawning can happen before the systemd in the container + // is fully initialized. To spawn a new process in the container, we need the systemd + // in the container to be fully initialized and listening for connections. + // One way to resolve this issue is to test the connection to the container's systemd. + if try_open_container_bus(ns_name).is_ok() { + return Ok(()); + } + // wait for a while, sleep time follows a natural-logarithm distribution + sleep(Duration::from_secs_f32(((i + 1) as f32).ln().ceil())); } - /// Binds a host directory into the machine. - pub fn bind>(&self, host: P, guest: P, read_only: bool) -> Result<()> { - let host = host.as_ref(); - let guest = guest.as_ref(); + Err(anyhow!("Timeout waiting for container {}", ns_name)) +} - let conn = zbus::blocking::Connection::system()?; - let proxy = ManagerProxyBlocking::new(&conn)?; - fs::create_dir_all(host)?; +/// Setting up cross-namespace bind-mounts for the container using systemd +fn setup_bind_mounts(ns_name: &str, mounts: &[(String, &str)]) -> Result<()> { + let conn = Connection::system()?; + let proxy = ManagerProxyBlocking::new(&conn)?; + for mount in mounts { + fs::create_dir_all(&mount.0)?; + let source_path = fs::canonicalize(&mount.0)?; proxy.bind_mount_machine( - self.name(), - &fs::canonicalize(host)?.to_string_lossy(), - &guest.to_string_lossy(), - read_only, + ns_name, + &source_path.to_string_lossy(), + mount.1, + false, true, )?; - Ok(()) } - /// Sends a poweroff signal to the machine, but does not wait. - pub fn poweroff(&self) -> Result<()> { - let exit_code = Command::new("systemd-run") - .env("SYSTEMD_ADJUST_TERMINAL_TITLE", "0") - .args(["-M", self.name(), "-q", "--no-block", "--", "poweroff"]) - .spawn()? - .wait()?; - if exit_code.success() { - Ok(()) - } else { - Err(Error::SubcommandError(exit_code)) - } + Ok(()) +} + +/// Get the container name (ns_name) of the instance +pub fn get_container_ns_name>(path: P, legacy: bool) -> Result { + let current_dir = std::env::current_dir()?; + let path = current_dir.join(path); + if legacy { + warn!("You are working in a legacy workspace. Use `ciel init --upgrade` to upgrade."); + warn!("Please make sure to save your work before upgrading."); + return legacy_container_name(&path); } - /// Stops the machine. - /// - /// This will first try to send a poweroff signal through [Machine::poweroff], and - /// wait for the machine to go off. If timeout, SIGKILL will be sent to the container. - pub fn stop(&self) -> Result<()> { - info!("{}: stopping", self.name()); - let proxy = ManagerProxyBlocking::new(&self.dbus_conn)?; - let path = proxy.get_machine(self.name())?; - let machine_proxy = MachineProxyBlocking::builder(&self.dbus_conn) - .path(&path)? - .build()?; - - let _ = machine_proxy.receive_state_changed(); - if self.poweroff().is_ok() { - if wait_for_poweroff(&proxy, self.name()).is_ok() { - return Ok(()); - } - warn!( - "{}: container not responding to poweroff, sending SIGKILL ...", - self.name() - ); - } + new_container_name(&path) +} + +/// Spawn a new container using nspawn +pub fn spawn_container>( + ns_name: &str, + path: P, + extra_options: &[String], + mounts: &[(String, &str)], +) -> Result<()> { + let path = path + .as_ref() + .to_str() + .ok_or_else(|| anyhow!("Path contains invalid Unicode characters."))?; + let mut child = Command::new("systemd-nspawn") + .args(DEFAULT_NSPAWN_OPTIONS) + .args(extra_options) + .args(["-D", path, "-M", ns_name, "--"]) + .env("SYSTEMD_NSPAWN_TMPFS_TMP", "0") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + + info!("{}: waiting for container to start...", ns_name); + wait_for_container(&mut child, ns_name, 10)?; + info!("{}: setting up mounts...", ns_name); + if let Err(e) = setup_bind_mounts(ns_name, mounts) { + warn!("Failed to setup bind mounts: {:?}", e); + } + + Ok(()) +} + +/// Execute a command in the container +pub fn execute_container_command>(ns_name: &str, args: &[S]) -> Result { + let mut extra_options = vec!["--setenv=HOME=/root".to_string()]; + if std::env::var("CIEL_STAGE2").is_ok() { + extra_options.push("--setenv=ABSTAGE2=1".to_string()); + } + // TODO: maybe replace with systemd API cross-namespace call? + let exit_code = Command::new("systemd-run") + .env("SYSTEMD_ADJUST_TERMINAL_TITLE", "0") + .args(extra_options) + .args(["-M", ns_name, "-qt", "--"]) + .args(args) + .spawn()? + .wait()? + .code() + .unwrap_or(127); + + Ok(exit_code) +} + +/// Reap all the exited child processes +pub(crate) fn clean_child_process() { + let mut status = 0; + unsafe { waitpid(-1, &mut status, WNOHANG) }; +} + +fn kill_container(proxy: &MachineProxyBlocking) -> Result<()> { + proxy.kill("all", libc::SIGKILL)?; + proxy.terminate()?; - machine_proxy.kill("all", nix::sys::signal::SIGKILL as i32)?; - wait_for_poweroff(&proxy, self.name())?; - machine_proxy.terminate()?; - proxy.terminate_machine(self.name())?; + Ok(()) +} + +fn execute_poweroff(ns_name: &str) -> Result<()> { + // TODO: maybe replace with systemd API cross-namespace call? + let exit_code = Command::new("systemd-run") + .env("SYSTEMD_ADJUST_TERMINAL_TITLE", "0") + .args(["-M", ns_name, "-q", "--no-block", "--", "poweroff"]) + .spawn()? + .wait()? + .code() + .unwrap_or(127); + if exit_code != 0 { + Err(anyhow!("Could not execute shutdown command: {}", exit_code)) + } else { Ok(()) } +} - /// Executes a command in the machine. - pub fn exec(&self, args: I) -> Result - where - I: IntoIterator, - S: AsRef, - { - if self.state()?.is_down() { - return Err(Error::ImproperState); +fn wait_for_poweroff(proxy: &ManagerProxyBlocking, ns_name: &str) -> Result<()> { + for _ in 0..10 { + if proxy.get_machine(ns_name).is_err() { + // machine object no longer exists + return Ok(()); } - // FIXME: maybe replace with systemd API cross-namespace call? - let mut child = Command::new("systemd-run"); - child - .env("SYSTEMD_ADJUST_TERMINAL_TITLE", "0") - .args(["-M", self.name(), "-qt", "--setenv=HOME=/root", "--"]) - .args(args); - debug!( - "invoking systemd-run: {:?}", - child.get_args().collect::>().join(&OsStr::new(" ")) - ); - Ok(child.spawn()?.wait()?) + sleep(Duration::from_secs(1)); } - /// Executes a command in the machine, capturing stdout and stderr. - pub fn exec_capture(&self, args: I) -> Result - where - I: IntoIterator, - S: AsRef, - { - if self.state()?.is_down() { - return Err(Error::ImproperState); - } - // FIXME: maybe replace with systemd API cross-namespace call? - let mut child = Command::new("systemd-run"); - child - .env("SYSTEMD_ADJUST_TERMINAL_TITLE", "0") - .args(["-M", self.name(), "-qt", "--setenv=HOME=/root", "--"]) - .args(args) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()); - debug!( - "invoking systemd-run: {:?}", - child.get_args().collect::>().join(&OsStr::new(" ")) - ); - let mut child = child.spawn()?; - let status = child.wait()?; - Ok(ExecResult { - status, - stdout: std::io::read_to_string(child.stdout.unwrap())?, - stderr: std::io::read_to_string(child.stderr.unwrap())?, - }) + Err(anyhow!("shutdown failed")) +} + +fn is_booted(proxy: &MachineProxyBlocking) -> Result { + let leader_pid = proxy.leader()?; + // let's inspect the cmdline of the PID 1 in the container + let f = std::fs::read(format!("/proc/{}/cmdline", leader_pid))?; + // take until the first null byte + let pos: usize = f + .iter() + .position(|c| *c == 0u8) + .ok_or_else(|| anyhow!("Unable to parse the process cmdline of PID 1 in the container"))?; + // ... well, of course it's a path + let path = Path::new(OsStr::from_bytes(&f[..pos])); + let exe_name = path.file_name(); + // if PID 1 is systemd or init (System V init) then it should be a "booted" container + if let Some(exe_name) = exe_name { + return Ok(exe_name == "systemd" || exe_name == "init"); } - /// Updates the system of the machine with oma or APT. - pub fn update_system(&self, apt: Option) -> Result<()> { - let apt = apt.unwrap_or(self.config.workspace_config.use_apt); - let script = if apt { - APT_UPDATE_SCRIPT - } else { - OMA_UPDATE_SCRIPT - }; - if apt { - let status = self.exec(["/usr/bin/bash", "-ec", script])?; - if !status.success() { - Err(Error::SubcommandError(status)) - } else { - Ok(()) - } - } else { - if !self.exec(["/usr/bin/bash", "-ec", script])?.success() { - warn!( - "{}: failed to update OS with oma, falling back to apt", - self.name() - ); - self.update_system(Some(true)) - } else { - Ok(()) - } + Ok(false) +} + +fn terminate_container( + proxy: &ManagerProxyBlocking, + machine_proxy: &MachineProxyBlocking, + ns_name: &str, +) -> Result<()> { + let _ = machine_proxy.receive_state_changed(); + if execute_poweroff(ns_name).is_ok() { + // Successfully passed poweroff command to the container, wait for it + if wait_for_poweroff(proxy, ns_name).is_ok() { + return Ok(()); } + // still did not poweroff? + warn!("Container did not respond to the poweroff command correctly..."); + warn!("Killing the container by sending SIGKILL..."); + // fall back to nuke + } + + // violently kill everything inside the container + kill_container(machine_proxy)?; + machine_proxy.terminate().ok(); + // status re-check, in the event of I/O problems, the container may still be running (stuck) + if wait_for_poweroff(proxy, ns_name).is_ok() { + return Ok(()); } + + Err(anyhow!("Failed to kill the container! This may indicate a problem with your I/O, see dmesg or journalctl for more details.")) } -const APT_UPDATE_SCRIPT: &str = r#"set -euo pipefail;export DEBIAN_FRONTEND=noninteractive;apt-get update -y --allow-releaseinfo-change && apt-get -y -o Dpkg::Options::="--force-confnew" full-upgrade --autoremove --purge && apt autoclean"#; -const OMA_UPDATE_SCRIPT: &str = r#"set -euo pipefail;oma upgrade -y --force-confnew --no-progress --force-unsafe-io && oma autoremove --no-progress -y --remove-config && oma clean --no-progress"#; +/// Terminate the container (Use graceful method if possible) +pub fn terminate_container_by_name(ns_name: &str) -> Result<()> { + let conn = Connection::system()?; + let proxy = ManagerProxyBlocking::new(&conn)?; + let path = proxy.get_machine(ns_name)?; + let machine_proxy = MachineProxyBlocking::builder(&conn).path(&path)?.build()?; -fn wait_for_machine(mut child: Child, ns_name: &str) -> Result<()> { - for i in 0..10 { - let exited = child.try_wait()?; - if let Some(status) = exited { - return Err(Error::SubcommandError(status)); - } - // PTY spawning may happen before the systemd in the container is fully initialized. - // To spawn a new process in the container, we need the systemd - // in the container to be fully initialized and listening for connections. - // One way to resolve this issue is to test the connection to the container's systemd. - { - // There are bunch of trickeries happening here - // First we initialize an empty pointer - let mut buf = MaybeUninit::uninit(); - // Convert the ns_name to C-style `const char*` (NUL-terminated) - let ns_name = CString::new(ns_name).unwrap(); - // unsafe: these functions are from libsystemd, which involving FFI calls - unsafe { - use libsystemd_sys::bus::{sd_bus_flush_close_unref, sd_bus_open_system_machine}; - // Try opening a connection to the container - if sd_bus_open_system_machine(buf.as_mut_ptr(), ns_name.as_ptr()) >= 0 { - // If successful, just close the connection and drop the pointer - sd_bus_flush_close_unref(buf.assume_init()); - return Ok(()); - } + terminate_container(&proxy, &machine_proxy, ns_name) +} + +/// Mount the filesystem layers using the specified layer manager and the instance name +pub fn mount_layers(manager: &mut dyn LayerManager, name: &str) -> Result<()> { + let target = std::env::current_dir()?.join(name); + if !manager.is_mounted(&target)? { + fs::create_dir_all(&target)?; + manager.mount(&target)?; + } + + Ok(()) +} + +/// Get the information of the container specified +pub fn inspect_instance(name: &str, ns_name: &str) -> Result { + let full_path = std::env::current_dir()?.join(name); + let mounted = is_mounted(&full_path, OsStr::new("overlay"))?; + let conn = Connection::system()?; + let proxy = ManagerProxyBlocking::new(&conn)?; + let path = proxy.get_machine(ns_name); + if let Err(e) = path { + if let zbus::Error::MethodError(ref err_name, _, _) = e { + if err_name.as_ref() == "org.freedesktop.machine1.NoSuchMachine" { + return Ok(CielInstance { + name: name.to_owned(), + ns_name: ns_name.to_owned(), + started: false, + running: false, + mounted, + booted: None, + }); } } - std::thread::sleep(Duration::from_secs_f32(((i + 1) as f32).ln().ceil())); + // For all other errors, just return the original error object + return Err(anyhow!("{}", e)); } - Err(Error::BootTimeout) + let path = path?; + let proxy = MachineProxyBlocking::builder(&conn).path(&path)?.build()?; + let state = proxy.state()?; + // Sometimes the system in the container is misconfigured, so we also accept "degraded" status as "running" + let running = state == "running" || state == "degraded"; + let booted = is_booted(&proxy)?; + + Ok(CielInstance { + name: name.to_owned(), + ns_name: ns_name.to_owned(), + started: true, + running, + mounted, + booted: Some(booted), + }) } -fn wait_for_poweroff(proxy: &ManagerProxyBlocking, name: &str) -> Result<()> { - for i in 0..10 { - if proxy.get_machine(name).is_err() { - return Ok(()); +/// List all the instances under the current directory +pub fn list_instances() -> Result> { + let legacy = is_legacy_workspace()?; + let mut instances: Vec = Vec::new(); + for entry in (fs::read_dir(CIEL_INST_DIR)?).flatten() { + if entry.file_type().map(|e| e.is_dir())? { + instances.push(inspect_instance( + &entry.file_name().to_string_lossy(), + &get_container_ns_name(entry.file_name(), legacy)?, + )?); } - std::thread::sleep(Duration::from_secs_f32(((i + 1) as f32).ln().ceil())); } - Err(Error::PoweroffTimeout) -} -/// The state of a machine. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub enum MachineState { - /// The machine is down. - Down, - /// The machine is starting. - Starting, - /// The machine is booted. - Running, + Ok(instances) } -impl MachineState { - pub fn is_down(&self) -> bool { - matches!(self, Self::Down) +/// List all the instances under the current directory, returns only instance names +pub fn list_instances_simple() -> Result> { + let mut instances: Vec = Vec::new(); + for entry in (fs::read_dir(CIEL_INST_DIR)?).flatten() { + if entry.file_type().map(|e| e.is_dir())? { + instances.push(entry.file_name().to_string_lossy().to_string()); + } } - pub fn is_starting(&self) -> bool { - matches!(self, Self::Starting) - } + Ok(instances) +} - pub fn is_running(&self) -> bool { - matches!(self, Self::Running) +/// Print all the instances under the current directory +pub fn print_instances() -> Result<()> { + use crate::logging::color_bool; + use std::io::Write; + use tabwriter::TabWriter; + + let instances = list_instances()?; + let mut formatter = TabWriter::new(std::io::stderr()); + writeln!(&mut formatter, "NAME\tMOUNTED\tRUNNING\tBOOTED")?; + for instance in instances { + let mounted = color_bool(instance.mounted); + let running = color_bool(instance.running); + let booted = { + if let Some(booted) = instance.booted { + color_bool(booted) + } else { + // dim + "\x1b[2m-\x1b[0m" + } + }; + writeln!( + &mut formatter, + "{}\t{}\t{}\t{}", + instance.name, mounted, running, booted + )?; } + formatter.flush()?; + + Ok(()) } -pub struct ExecResult { - pub status: ExitStatus, - pub stdout: String, - pub stderr: String, +#[test] +fn test_inspect_instance() { + println!("{:#?}", inspect_instance("alpine", "alpine")); } -#[cfg(test)] -mod test { - use crate::test::{TestDir, is_root}; - use test_log::test; - - #[test(ignore)] - fn test_container_boot() { - let testdir = TestDir::from("testdata/simple-workspace"); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - assert!(ws.is_system_loaded()); - let inst = ws.instance("test").unwrap(); - dbg!(&inst); - let container = inst.open().unwrap(); - dbg!(&container); - assert!(container.state().unwrap().is_down()); - if is_root() { - container.boot().unwrap(); - assert!(container.state().unwrap().is_running()); - container.stop(true).unwrap(); - } - } +#[test] +fn test_container_name() { + assert_eq!( + get_container_ns_name(Path::new("/tmp/"), false).unwrap(), + "tmp-51601b0".to_string() + ); + println!( + "{:#?}", + get_container_ns_name(Path::new("/tmp/"), true).unwrap() + ); } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..46e13ac --- /dev/null +++ b/src/main.rs @@ -0,0 +1,402 @@ +mod actions; +mod cli; +mod common; +mod config; +mod dbus_machine1; +mod dbus_machine1_machine; +mod diagnose; +mod logging; +mod machine; +mod network; +mod overlayfs; +mod repo; + +use anyhow::{anyhow, bail, Context, Result}; +use clap::ArgMatches; +use config::read_config; +use console::{style, user_attended}; +use dotenvy::dotenv; +use std::process; +use std::{path::Path, process::Command}; + +use crate::actions::BuildSettings; +use crate::common::*; + +macro_rules! print_error { + ($input:block) => { + if let Err(e) = $input { + error!("{:?}", e); + process::exit(1); + } + }; +} + +macro_rules! one_or_all_instance { + ($args:ident, $func:expr) => {{ + if let Ok(instance) = get_instance_option($args) { + $func(&instance) + } else { + actions::for_each_instance($func) + } + }}; +} + +fn unsupported_target_architecture(arch: &str) -> ! { + error!("Unknown target architecture {}", arch); + info!("Supported target architectures:"); + eprintln!( + "{}\n{}", + CIEL_MAINLINE_ARCHS.join("\n\t"), + CIEL_RETRO_ARCHS.join("\n\t") + ); + info!("If you do want to load an OS unsupported by Ciel, specify a tarball to initialize this workspace."); + process::exit(1); +} + +fn get_output_dir() -> String { + if let Ok(c) = config::read_config() { + return actions::get_output_directory(c.sep_mount); + } + "OUTPUT".to_string() +} + +#[inline] +fn get_instance_option(args: &ArgMatches) -> Result { + let option_instance = args.get_one::("INSTANCE"); + if option_instance.is_none() { + return Err(anyhow!("No instance specified!")); + } + + Ok(option_instance.expect("Internal error").to_string()) +} + +#[inline] +fn is_root() -> bool { + nix::unistd::geteuid().is_root() +} + +fn update_tree(path: &Path, branch: Option<&String>, rebase_from: Option<&String>) -> Result<()> { + let mut repo = network::fetch_repo(path)?; + if let Some(branch) = branch { + if repo.state() != git2::RepositoryState::Clean { + bail!( + "Cannot switch branches, because your tree seems to have an operation in progress." + ); + } + let result = network::git_switch_branch(&mut repo, branch, rebase_from.map(|x| x.as_str())); + if let Err(e) = result { + bail!("Failed to switch branches: {}\nNote that you can still use `git stash pop` to retrieve your previous changes.`", e); + } + info!("Successfully updated the tree and switched to {}.", branch); + } else { + if rebase_from.is_some() { + bail!("You need to specify a branch to switch to when requesting a rebase."); + } + info!("Successfully fetched new changes from remote."); + } + + Ok(()) +} + +fn main() -> Result<()> { + // set umask to 022 to ensure correct permissions on rootfs + unsafe { + libc::umask(libc::S_IWGRP | libc::S_IWOTH); + } + + // source .env file, ignore errors + dotenv().ok(); + + let build_cli = cli::build_cli(); + let version_string = build_cli.render_version(); + let args = build_cli.get_matches(); + if !is_root() { + println!("Please run me as root!"); + process::exit(1); + } + let mut directory = Path::new(args.get_one::("C").unwrap()).to_path_buf(); + let host_arch = get_host_arch_name(); + // Switch to the target directory + std::env::set_current_dir(&directory).unwrap(); + // get subcommands from command line parser + let subcmd = args.subcommand(); + // check if the workspace exists, except when the command is `init` or `new` + match subcmd { + Some(("init", _)) | Some(("new", _)) | Some(("version", _)) => (), + _ if !Path::new("./.ciel").is_dir() => { + if directory == Path::new(".") { + directory = + common::find_ciel_dir(".").context("Error finding ciel workspace directory")?; + info!( + "Selected Ciel directory: {}", + style(directory.canonicalize()?.display()).cyan() + ); + std::env::set_current_dir(&directory).unwrap(); + } else { + error!("This directory does not look like a Ciel workspace"); + process::exit(1); + } + } + _ => (), + } + // list instances if no command is specified + if subcmd.is_none() { + machine::print_instances()?; + return Ok(()); + } + let subcmd = subcmd.unwrap(); + // Switch table + match subcmd { + ("farewell", _) => { + actions::farewell(&directory).unwrap(); + } + ("init", args) => { + if args.get_flag("upgrade") { + info!("Upgrading workspace..."); + info!("First, shutting down all the instances..."); + print_error!({ actions::for_each_instance(&actions::container_down) }); + } else { + warn!("Please do not use this command manually ..."); + warn!("... try `ciel new` instead."); + } + print_error!({ common::ciel_init() }); + info!("Initialized working directory at {}", directory.display()); + } + ("load-tree", args) => { + info!("Cloning abbs tree..."); + network::download_git(args.get_one::("url").unwrap(), Path::new("TREE"))?; + } + ("update-tree", args) => { + let tree = Path::new("TREE"); + info!("Updating tree..."); + print_error!({ update_tree(tree, args.get_one("branch"), args.get_one("rebase")) }); + } + ("load-os", args) => { + let url = args.get_one::("url"); + if let Some(url) = url { + let use_tarball = !url.ends_with(".squashfs"); + // load from network using specified url + if url.starts_with("https://") || url.starts_with("http://") { + print_error!({ actions::load_os(url, None, use_tarball) }); + return Ok(()); + } + // load from file + let tarball = Path::new(url); + if !tarball.is_file() { + error!("{:?} is not a file", url); + process::exit(1); + } + print_error!({ + common::extract_system_rootfs(tarball, tarball.metadata()?.len(), use_tarball) + }); + + return Ok(()); + } + // load from network using auto picked url + let specified_arch = args.get_one::("arch"); + let arch = if let Some(specified_arch) = specified_arch { + if !check_arch_name(specified_arch.as_str()) { + unsupported_target_architecture(specified_arch.as_str()); + } + specified_arch + } else if !user_attended() { + host_arch + .ok_or_else(|| anyhow!("Ciel does not support this CPU architecture.")) + .unwrap() + } else { + ask_for_target_arch().unwrap() + }; + info!("Picking OS tarball for architecture {}", arch); + let rootfs = network::pick_latest_rootfs(arch); + + if let Err(e) = rootfs { + error!("Unable to determine the latest tarball: {}", e); + process::exit(1); + } + + let rootfs = rootfs.unwrap(); + print_error!({ + actions::load_os( + &format!("https://releases.aosc.io/{}", rootfs.path), + Some(rootfs.sha256sum), + false, + ) + }); + } + ("update-os", args) => { + let force_use_apt = if get_host_arch_name().is_some_and(|x| x == "riscv64") { + true + } else { + args.get_flag("force_use_apt") || read_config().is_ok_and(|x| x.force_use_apt) + }; + + print_error!({ actions::update_os(force_use_apt,) }); + } + ("config", args) => { + if args.get_flag("g") { + print_error!({ actions::config_os(None) }); + return Ok(()); + } + let instance = get_instance_option(args)?; + print_error!({ actions::config_os(Some(&instance)) }); + } + ("mount", args) => { + print_error!({ one_or_all_instance!(args, &actions::mount_fs) }); + } + ("new", args) => { + let arch = args.get_one::("arch").map(|val| { + if !check_arch_name(val) { + unsupported_target_architecture(val.as_str()); + } + val.as_str() + }); + let tarball = args.get_one::("tarball"); + if let Err(e) = actions::onboarding(tarball, arch) { + error!("{}", e); + process::exit(1); + } + } + ("run", args) => { + let instance = get_instance_option(args)?; + let args = args.get_many::("COMMANDS").unwrap(); + let status = + actions::run_in_container(&instance, &args.into_iter().collect::>())?; + process::exit(status); + } + ("shell", args) => { + let instance = get_instance_option(args)?; + if let Some(cmd) = args.get_many::("COMMANDS") { + let command = cmd + .into_iter() + .fold(String::with_capacity(1024), |acc, x| acc + " " + x); + let status = actions::run_in_container(&instance, &["/bin/bash", "-ec", &command])?; + process::exit(status); + } + let status = actions::run_in_container(&instance, &["/bin/bash"])?; + process::exit(status); + } + ("stop", args) => { + let instance = get_instance_option(args)?; + print_error!({ actions::stop_container(&instance) }); + } + ("down", args) => { + print_error!({ one_or_all_instance!(args, &actions::container_down) }); + } + ("commit", args) => { + let instance = get_instance_option(args)?; + print_error!({ actions::commit_container(&instance) }); + } + ("rollback", args) => { + print_error!({ one_or_all_instance!(args, &actions::rollback_container) }); + } + ("del", args) => { + let instance = args.get_one::("INSTANCE").unwrap(); + print_error!({ actions::remove_instance(instance) }); + } + ("add", args) => { + let instance = args.get_one::("INSTANCE").unwrap(); + print_error!({ actions::add_instance(instance) }); + } + ("build", args) => { + let instance = get_instance_option(args)?; + let settings = BuildSettings { + offline: args.get_flag("OFFLINE"), + stage2: args.get_flag("STAGE2"), + }; + let mut state = None; + if let Some(cont) = args.get_one::("CONTINUE") { + state = Some(actions::load_build_checkpoint(cont)?); + let empty: Vec<&str> = Vec::new(); + let status = actions::package_build(&instance, empty.into_iter(), state, settings)?; + println!("\x07"); // bell character + process::exit(status); + } + let packages = args.get_many::("PACKAGES"); + if packages.is_none() { + error!("Please specify a list of packages to build!"); + process::exit(1); + } + let packages = packages.unwrap(); + if args.contains_id("SELECT") { + let start_package = args.get_one::("SELECT"); + let status = + actions::packages_stage_select(&instance, packages, settings, start_package)?; + process::exit(status); + } + if args.get_flag("FETCH") { + let packages = packages.into_iter().collect::>(); + let status = actions::package_fetch(&instance, &packages)?; + process::exit(status); + } + let status = actions::package_build(&instance, packages, state, settings)?; + println!("\x07"); // bell character + process::exit(status); + } + ("", _) => { + machine::print_instances()?; + } + ("list", _) => { + machine::print_instances()?; + } + ("doctor", _) => { + print_error!({ diagnose::run_diagnose() }); + } + ("repo", args) => match args.subcommand() { + Some(("refresh", _)) => { + info!("Refreshing repository..."); + print_error!({ + repo::refresh_repo(&std::env::current_dir().unwrap().join(get_output_dir())) + }); + info!("Repository has been refreshed."); + } + Some(("init", args)) => { + info!("Initializing repository..."); + let instance = get_instance_option(args)?; + let cwd = std::env::current_dir().unwrap(); + print_error!({ actions::mount_fs(&instance) }); + print_error!({ repo::init_repo(&cwd.join(get_output_dir()), &cwd.join(instance)) }); + info!("Repository has been initialized and refreshed."); + } + Some(("deinit", args)) => { + info!("Disabling local repository..."); + let instance = get_instance_option(args)?; + let cwd = std::env::current_dir().unwrap(); + print_error!({ actions::mount_fs(&instance) }); + print_error!({ repo::deinit_repo(&cwd.join(instance)) }); + info!("Repository has been disabled."); + } + _ => unreachable!(), + }, + ("clean", _) => { + print_error!({ actions::cleanup_outputs() }); + } + ("version", _) => { + println!("{}", version_string); + } + // catch all other conditions + (_, options) => { + let exe_dir = std::env::current_exe()?; + let exe_dir = exe_dir.parent().expect("Where am I?"); + let cmd = args.subcommand().unwrap().0; + let plugin = exe_dir + .join("../libexec/ciel-plugin/") + .join(format!("ciel-{}", cmd)); + if !plugin.is_file() { + error!("Unknown command: `{}`.", cmd); + process::exit(1); + } + info!("Executing applet ciel-{}", cmd); + let mut process = &mut Command::new(plugin); + if let Some(args) = options.get_many::("COMMANDS") { + process = process.args(args); + } + let status = process.status().unwrap().code().unwrap(); + if status != 0 { + error!("Applet exited with error {}", status); + } + process::exit(status); + } + } + + Ok(()) +} diff --git a/cli/src/download.rs b/src/network.rs similarity index 53% rename from cli/src/download.rs rename to src/network.rs index b373587..6635862 100644 --- a/cli/src/download.rs +++ b/src/network.rs @@ -1,86 +1,68 @@ +use crate::make_progress_bar; +use anyhow::{anyhow, Result}; +use fs3::FileExt; +use reqwest::blocking::{Client, Response}; +use serde::Deserialize; +use std::path::Path; +use std::sync::LazyLock; use std::{ - path::Path, sync::{ atomic::{AtomicUsize, Ordering}, - Arc, LazyLock, + Arc, }, - thread, + thread::{self, sleep}, time::Duration, }; -use anyhow::{anyhow, bail, Result}; -use log::info; -use reqwest::header::CONTENT_LENGTH; -use serde::{Deserialize, Serialize}; - -use crate::make_progress_bar; - const MANIFEST_URL: &str = "https://releases.aosc.io/manifest/recipe.json"; -pub const CIEL_MAINLINE_ARCHS: &[&str] = &[ - "amd64", - "arm64", - "ppc64el", - "mips64r6el", - "riscv64", - "loongarch64", - "loongson3", -]; -pub const CIEL_RETRO_ARCHS: &[&str] = &["armv4", "armv6hf", "armv7hf", "i486", "m68k", "powerpc"]; - -/// AOSC OS release manifest. -/// -/// This should be kept in sync with the structure of release manifest -/// (`https://releases.aosc.io/manifest/recipe.json`) -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Recipe { - pub version: usize, - pub variants: Vec, +#[derive(Deserialize, Debug, Clone)] +pub struct RootFs { + pub arch: String, + pub date: String, + pub path: String, + pub sha256sum: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Deserialize)] pub struct Variant { - pub name: String, - pub squashfs: Vec, + name: String, + squashfs: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RootFsInfo { - pub arch: String, - pub date: String, - pub path: String, - pub sha256sum: String, +/// AOSC OS Tarball Recipe structure +#[derive(Deserialize)] +pub struct Recipe { + pub version: usize, + variants: Vec, } -pub fn http_client() -> Result { - Ok(reqwest::blocking::Client::builder() - .user_agent(concat!( - env!("CARGO_PKG_NAME"), - "/", - env!("CARGO_PKG_VERSION"), - )) - .build()?) +static GIT_PROGRESS: LazyLock = LazyLock::new(|| { + indicatif::ProgressStyle::default_bar() + .template("[{bar:25.cyan/blue}] {pos}/{len} {msg} ({eta})") + .unwrap() +}); + +/// Download a file from the web +pub fn download_file(url: &str) -> Result { + let client = Client::new().get(url).send()?; + + Ok(client) } /// Download a file with progress indicator -pub fn download_file(url: &str, file: &Path) -> Result<()> { +pub fn download_file_progress(url: &str, file: &str) -> Result { let mut output = std::fs::File::create(file)?; - let resp = http_client()?.get(url).send()?; - + let resp = download_file(url)?; let mut total: u64 = 0; - if let Some(length) = resp.headers().get(CONTENT_LENGTH) { - total = length - .to_str() - .ok() - .and_then(|s| s.parse::().ok()) - .unwrap_or(0); + if let Some(length) = resp.headers().get("content-length") { + total = length.to_str().unwrap_or("0").parse::().unwrap_or(0); } if total > 0 { // pre-allocate all the required disk space, // fails early when there is insufficient disk space available - fs3::FileExt::allocate(&output, total)?; + output.allocate(total)?; } - let progress_bar = indicatif::ProgressBar::new(total); progress_bar.set_style( indicatif::ProgressStyle::default_bar() @@ -92,42 +74,33 @@ pub fn download_file(url: &str, file: &Path) -> Result<()> { std::io::copy(&mut reader, &mut output)?; progress_bar.finish_and_clear(); - Ok(()) + Ok(total) } -/// Pick the latest BuildKit rootfs according to the recipe -pub fn pick_latest_rootfs(arch: &str) -> Result { - info!("Picking latest BuildKit for {}", arch); - let resp = http_client()? - .get(MANIFEST_URL) - .send()? - .error_for_status()? - .json::()?; - - let buildkit = resp +/// Pick the latest buildkit rootfs according to the recipe +pub fn pick_latest_rootfs(arch: &str) -> Result { + let resp = Client::new().get(MANIFEST_URL).send()?; + let recipe: Recipe = resp.json()?; + let buildkit = recipe .variants .into_iter() .find(|v| v.name == "BuildKit") - .ok_or_else(|| anyhow!("Unable to find BuildKit variant"))?; - let mut rootfs: Vec = buildkit + .ok_or_else(|| anyhow!("Unable to find buildkit variant"))?; + + let mut rootfs: Vec = buildkit .squashfs .into_iter() .filter(|rootfs| rootfs.arch == arch) .collect(); if rootfs.is_empty() { - bail!("No suitable squashfs was found") + return Err(anyhow!("No suitable squashfs was found")); } rootfs.sort_unstable_by_key(|x| x.date.clone()); + Ok(rootfs.last().unwrap().to_owned()) } -static GIT_PROGRESS: LazyLock = LazyLock::new(|| { - indicatif::ProgressStyle::default_bar() - .template("[{bar:25.cyan/blue}] {pos}/{len} {msg} ({eta})") - .unwrap() -}); - /// Clone the Git repository to `root` pub fn download_git(uri: &str, root: &Path) -> Result<()> { let mut callbacks = git2::RemoteCallbacks::new(); @@ -188,7 +161,7 @@ pub fn download_git(uri: &str, root: &Path) -> Result<()> { 2 => progress.set_message("Checking out files..."), _ => break, } - std::thread::sleep(Duration::from_millis(100)); + sleep(Duration::from_millis(100)); } progress.finish_and_clear(); }); @@ -202,3 +175,79 @@ pub fn download_git(uri: &str, root: &Path) -> Result<()> { Ok(()) } + +// other Git operations +fn find_branch<'a>(repo: &'a git2::Repository, name: &str) -> Result> { + let branch = repo.find_branch(name, git2::BranchType::Local); + if let Ok(branch) = branch { + return Ok(branch); + } + let remote_branch = repo.find_branch(&format!("origin/{}", name), git2::BranchType::Remote); + if let Ok(branch) = remote_branch { + let target_commit = branch.get().peel_to_commit()?; + let branch = repo.branch(name, &target_commit, false)?; + return Ok(branch); + } + + Err(anyhow!("Could not find branch `{}'", name)) +} + +pub fn fetch_repo>(path: P) -> Result { + let repo = git2::Repository::open(path.as_ref())?; + let mut remote = repo.find_remote("origin")?; + let refs = remote.fetch_refspecs()?; + let refspecs = refs.into_iter().flatten().collect::>(); + let mut opts = git2::FetchOptions::new(); + opts.prune(git2::FetchPrune::On); + remote.fetch(&refspecs, Some(&mut opts), None)?; + drop(remote); // dis-own the variable `repo` + + Ok(repo) +} + +pub fn git_switch_branch( + repo: &mut git2::Repository, + branch: &str, + rebase_from: Option<&str>, +) -> Result { + let target_branch = find_branch(repo, branch).unwrap(); + let branch_ref = target_branch.into_reference(); + let branch_refname = branch_ref.name().unwrap().to_string(); + drop(branch_ref); + let stasher = git2::Signature::now("ciel", "bot@aosc.io")?; + let repo_statuses = repo.statuses(None)?; + let is_tree_dirty = !repo_statuses.is_empty(); + drop(repo_statuses); + if is_tree_dirty { + repo.stash_save( + &stasher, + "ciel auto save", + Some(git2::StashFlags::INCLUDE_UNTRACKED), + )?; + } + repo.set_head(&branch_refname)?; + let mut opts = git2::build::CheckoutBuilder::new(); + repo.checkout_head(Some(opts.force()))?; + repo.cleanup_state()?; + if is_tree_dirty && rebase_from.is_none() { + repo.stash_pop(0, None)?; + } + if let Some(rebase_upstream) = rebase_from { + // attempt rebase + let status = std::process::Command::new("git") + .args(["rebase", rebase_upstream]) + .current_dir(repo.workdir().unwrap()) + .spawn()? + .wait()?; + if !status.success() { + return Err(anyhow!("Error performing rebase")); + } + repo.cleanup_state()?; + if is_tree_dirty { + repo.stash_pop(0, None)?; + } + } + + // returns whether a stash was made + Ok(is_tree_dirty) +} diff --git a/src/overlayfs.rs b/src/overlayfs.rs new file mode 100644 index 0000000..f182df8 --- /dev/null +++ b/src/overlayfs.rs @@ -0,0 +1,416 @@ +use crate::common; +use anyhow::{anyhow, bail, Context, Result}; +use libmount::{mountinfo::Parser, Overlay}; +use nix::mount::{umount2, MntFlags}; +use std::fs; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::{ + ffi::OsStr, + io::{BufRead, BufReader}, +}; + +pub trait LayerManager { + /// Return the name of the layer manager, e.g. "overlay". + /// This name should be the same as the fs_type listed in the /proc/<>/mountinfo file + fn name() -> String + where + Self: Sized; + /// Create a new layer manager from the given distribution directory + /// dist: distribution directory, inst: instance name (not directory) + fn from_inst_dir>( + dist_path: P, + inst_path: P, + inst_name: P, + ) -> Result> + where + Self: Sized; + /// Mount the filesystem to the given path + fn mount(&mut self, to: &Path) -> Result<()>; + /// Return if the filesystem is mounted + fn is_mounted(&self, target: &Path) -> Result; + /// Rollback the filesystem to the distribution state + fn rollback(&mut self) -> Result<()>; + /// Commit the current state of the instance filesystem to the distribution state + fn commit(&mut self) -> Result<()>; + /// Un-mount the filesystem + fn unmount(&mut self, target: &Path) -> Result<()>; + /// Return the directory where the configuration layer is located + /// You may temporary mount this directory if your backend does not expose this directory directly + fn get_config_layer(&mut self) -> Result; + /// Return the directory where the base layer is located + fn get_base_layer(&mut self) -> Result; + /// Set the volatile state of the instance filesystem + fn set_volatile(&mut self, volatile: bool) -> Result<()>; + /// Destroy the filesystem of the current instance + fn destroy(&mut self) -> Result<()>; +} + +struct OverlayFS { + inst: PathBuf, + base: PathBuf, + lower: PathBuf, + upper: PathBuf, + work: PathBuf, + volatile: bool, +} + +/// Create a new overlay filesystem on the host system +pub fn create_new_instance_fs>(inst_path: P, inst_name: P) -> Result<()> { + let inst = inst_path.as_ref().join(inst_name.as_ref()); + fs::create_dir_all(inst)?; + Ok(()) +} + +/// OverlayFS operations +#[derive(Debug)] +enum Diff { + Symlink(PathBuf), + OverrideDir(PathBuf), + RenamedDir(PathBuf, PathBuf), + NewDir(PathBuf), + ModifiedDir(PathBuf), // Modify permission only + WhiteoutFile(PathBuf), // Dir or File + File(PathBuf), // Simple modified or new file +} + +impl OverlayFS { + /// Generate a list of changes made in the upper layer + fn diff(&self) -> Result> { + let mut mods: Vec = Vec::new(); + let mut processed_dirs: Vec = Vec::new(); + + for entry in walkdir::WalkDir::new(&self.upper).into_iter().skip(1) { + // SKip the root + let path: PathBuf = entry?.path().to_path_buf(); + let rel_path = path.strip_prefix(&self.upper)?.to_path_buf(); + let lower_path = self.lower.join(&rel_path).to_path_buf(); + + if has_prefix(&rel_path, &processed_dirs) { + continue; // We already dealt with it + } + let meta = fs::symlink_metadata(&path)?; + let file_type = meta.file_type(); + + if file_type.is_symlink() { + // Just move the symlink + mods.push(Diff::Symlink(rel_path.clone())); + } else if meta.is_dir() { + // Deal with dirs + let opaque = xattr::get(&path, "trusted.overlay.opaque")?; + let redirect = xattr::get(&path, "trusted.overlay.redirect")?; + let metacopy = xattr::get(&path, "trusted.overlay.metacopy")?; + + if let Some(_data) = metacopy { + bail!("Unsupported filesystem feature: metacopy"); + } + if let Some(text) = opaque { + // the new dir (completely) replace the old one + if text == b"y" { + // Delete corresponding dir + mods.push(Diff::OverrideDir(rel_path.clone())); + processed_dirs.push(rel_path.clone()); + } + } else if let Some(from_utf8) = redirect { + // Renamed + let mut from_rel_path = PathBuf::from(OsStr::from_bytes(&from_utf8)); + if from_rel_path.is_absolute() { + // abs path from root of OverlayFS + from_rel_path = from_rel_path.strip_prefix("/")?.to_path_buf(); + } else { + // rel path, same parent dir as the origin + let mut from_path = path.clone(); + from_path.pop(); + from_path.push(PathBuf::from(&from_rel_path)); + from_rel_path = from_path.strip_prefix(&self.upper)?.to_path_buf(); + } + mods.push(Diff::RenamedDir(from_rel_path, rel_path)); + } else if !lower_path.is_dir() { + // New dir + mods.push(Diff::NewDir(rel_path.clone())); + } else { + // Modified + mods.push(Diff::ModifiedDir(rel_path.clone())); + } + } else { + // Deal with files + if file_type.is_char_device() && meta.rdev() == 0 { + // Whiteout file! + mods.push(Diff::WhiteoutFile(rel_path.clone())); + } else if lower_path.is_dir() { + // A new file overrides an old directory + mods.push(Diff::OverrideDir(rel_path.clone())); + } else { + mods.push(Diff::File(rel_path.clone())); + } + } + } + + Ok(mods) + } +} + +impl LayerManager for OverlayFS { + fn name() -> String + where + Self: Sized, + { + "overlay".to_owned() + } + // The overlayfs structure inherited from older CIEL looks like this: + // |- work: .ciel/container/instances//diff.tmp/ + // |- upper: .ciel/container/instances//diff/ + // |- lower: .ciel/container/instances//local/ + // ||- lower (base): .ciel/container/dist/ + fn from_inst_dir>( + dist_path: P, + inst_path: P, + inst_name: P, + ) -> Result> + where + Self: Sized, + { + let dist = dist_path.as_ref(); + let inst = inst_path.as_ref().join(inst_name.as_ref()); + Ok(Box::new(OverlayFS { + inst: inst.to_owned(), + base: dist.to_owned(), + lower: inst.join("layers/local"), + upper: inst.join("layers/diff"), + work: inst.join("layers/diff.tmp"), + volatile: false, + })) + } + fn mount(&mut self, to: &Path) -> Result<()> { + let base_dirs = [self.lower.clone(), self.base.clone()]; + let mut overlay = Overlay::writable( + // base_dirs variable contains the base and lower directories + base_dirs.iter().map(|x| x.as_ref()), + self.upper.clone(), + self.work.clone(), + to, + ); + // create the directories if they don't exist (work directory may be missing) + fs::create_dir_all(&self.work)?; + fs::create_dir_all(&self.upper)?; + fs::create_dir_all(&self.lower)?; + // check overlay usability + load_overlayfs_support()?; + if self.volatile { + overlay.set_options(b"volatile".to_vec()); + } + let dirty_flag = self.work.join("work/incompat"); + if dirty_flag.exists() { + return Err(anyhow!( + "This container filesystem can't be used anymore. Please rollback." + )); + } + // let's mount them + overlay.mount().map_err(|e| anyhow!("{}", e.to_string()))?; + + Ok(()) + } + + /// is_mounted: check if a path is a mountpoint with corresponding fs_type + fn is_mounted(&self, target: &Path) -> Result { + is_mounted(target, OsStr::new("overlay")) + } + + fn rollback(&mut self) -> Result<()> { + fs::remove_dir_all(&self.upper)?; + fs::remove_dir_all(&self.work)?; + fs::create_dir(&self.upper)?; + fs::create_dir(&self.work)?; + + Ok(()) + } + + fn commit(&mut self) -> Result<()> { + if self.volatile { + // for safety reasons + nix::unistd::sync(); + } + let mods = self.diff()?; + // FIXME: use drain_filter in the future + // first pass to execute all the deletion actions + for i in mods.iter() { + match i { + Diff::WhiteoutFile(_) => overlay_exec_action(i, self)?, + _ => continue, + } + } + // second pass for everything else + for i in mods.iter() { + match i { + Diff::WhiteoutFile(_) => continue, + _ => overlay_exec_action(i, self) + .with_context(|| format!("when processing {:?}", i))?, + } + } + // clear all the remnant items in the upper layer + self.rollback()?; + + Ok(()) + } + + fn unmount(&mut self, target: &Path) -> Result<()> { + umount2(target, MntFlags::MNT_DETACH)?; + + Ok(()) + } + + fn get_config_layer(&mut self) -> Result { + Ok(self.lower.clone()) + } + + fn get_base_layer(&mut self) -> Result { + Ok(self.base.clone()) + } + + fn destroy(&mut self) -> Result<()> { + fs::remove_dir_all(&self.inst)?; + + Ok(()) + } + + fn set_volatile(&mut self, volatile: bool) -> Result<()> { + self.volatile = volatile; + + Ok(()) + } +} + +/// is_mounted: check if a path is a mountpoint with corresponding fs_type +pub(crate) fn is_mounted(mountpoint: &Path, fs_type: &OsStr) -> Result { + let mountinfo_content: Vec = fs::read("/proc/self/mountinfo")?; + let parser = Parser::new(&mountinfo_content); + + for mount in parser { + let mount = mount?; + if mount.mount_point == mountpoint && mount.fstype == fs_type { + return Ok(true); + } + } + + Ok(false) +} + +/// A convenience function for getting a overlayfs type LayerManager +pub(crate) fn get_overlayfs_manager(inst_name: &str) -> Result> { + OverlayFS::from_inst_dir(common::CIEL_DIST_DIR, common::CIEL_INST_DIR, inst_name) +} + +/// Check if path have all specified prefixes (with order) +#[inline] +fn has_prefix(path: &Path, prefixes: &[PathBuf]) -> bool { + prefixes + .iter() + .any(|prefix| path.strip_prefix(prefix).is_ok()) +} + +fn load_overlayfs_support() -> Result<()> { + if test_overlay_usability().is_err() { + Command::new("modprobe") + .arg("overlay") + .status() + .map_err(|e| anyhow!("Unable to load overlay kernel module: {}", e))?; + } + + Ok(()) +} + +#[inline] +pub fn test_overlay_usability() -> Result<()> { + let f = fs::File::open("/proc/filesystems")?; + let reader = BufReader::new(f); + for line in reader.lines() { + let line = line?; + let mut fs_type = line.splitn(2, '\t'); + if let Some(fs_type) = fs_type.nth(1) { + if fs_type == "overlay" { + return Ok(()); + } + } + } + + Err(anyhow!("No overlayfs support detected")) +} + +/// Set permission of to according to from +#[inline] +fn sync_permission(from: &Path, to: &Path) -> Result<()> { + let from_meta = fs::metadata(from)?; + let to_meta = fs::metadata(to)?; + + if from_meta.mode() != to_meta.mode() { + to_meta.permissions().set_mode(to_meta.mode()); + } + + Ok(()) +} + +#[inline] +fn overlay_exec_action(action: &Diff, overlay: &OverlayFS) -> Result<()> { + match action { + Diff::Symlink(path) => { + let upper_path = overlay.upper.join(path); + let lower_path = overlay.base.join(path); + // Replace lower dir with upper + fs::rename(upper_path, lower_path)?; + } + Diff::OverrideDir(path) => { + let upper_path = overlay.upper.join(path); + let lower_path = overlay.base.join(path); + // Replace lower dir with upper + if lower_path.is_dir() { + // If exists and was not removed already, then remove it + fs::remove_dir_all(&lower_path)?; + } else if lower_path.is_file() { + // If it's a file, then remove it as well + fs::remove_file(&lower_path)?; + } + fs::rename(upper_path, &lower_path)?; + } + Diff::RenamedDir(from, to) => { + // TODO: Implement copy down + // Such dir will include diff files, so this + // section need more testing + let from_path = overlay.base.join(from); + let to_path = overlay.base.join(to); + // TODO: Merge files from upper to lower + // Replace lower dir with upper + fs::rename(from_path, to_path)?; + } + Diff::NewDir(path) => { + let lower_path = overlay.base.join(path); + // Construct lower path + fs::create_dir_all(lower_path)?; + } + Diff::ModifiedDir(path) => { + // Do nothing, just sync permission + let upper_path = overlay.upper.join(path); + let lower_path = overlay.base.join(path); + sync_permission(&upper_path, &lower_path)?; + } + Diff::WhiteoutFile(path) => { + let lower_path = overlay.base.join(path); + if lower_path.is_dir() { + fs::remove_dir_all(&lower_path)?; + } else if lower_path.is_file() { + fs::remove_file(&lower_path)?; + } + // remove the whiteout in the upper layer + fs::remove_file(overlay.upper.join(path))?; + } + Diff::File(path) => { + let upper_path = overlay.upper.join(path); + let lower_path = overlay.base.join(path); + // Move upper file to overwrite the lower + fs::rename(upper_path, lower_path)?; + } + } + + Ok(()) +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs index a066804..a1ca4fc 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,121 +1,70 @@ -use std::{ - fmt::Debug, - fs, - io::Write, - path::{Path, PathBuf}, -}; +//! Local repository -use faster_hex::hex_string; -use log::info; +use crate::info; +use anyhow::Result; +use console::style; use sha2::{Digest, Sha256}; +use std::io::Write; +use std::{fs, io, path::Path}; use time::{format_description::FormatItem, macros::format_description, OffsetDateTime}; -pub mod monitor; -pub mod scan; +mod monitor; +mod scan; -use crate::Result; +pub use monitor::start_monitor; /// Debian 822 date: "%a, %d %b %Y %H:%M:%S %z" -const DEB822_DATE: &[FormatItem] = format_description!( - "[weekday repr:short], [day] [month repr:short] [year] [hour repr:24]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]" -); - -/// A simple flat APT package repository. -#[derive(Clone)] -pub struct SimpleAptRepository { - path: PathBuf, -} - -impl Debug for SimpleAptRepository { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(&self.path, f) - } +const DEB822_DATE: &[FormatItem] = format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour repr:24]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]"); + +fn generate_release(path: &Path) -> Result { + let mut f = fs::File::open(path.join("Packages"))?; + let mut hasher = Sha256::new(); + io::copy(&mut f, &mut hasher)?; + let result = hasher.finalize(); + let meta = f.metadata()?; + let timestamp = OffsetDateTime::now_utc().format(&DEB822_DATE)?; + + Ok(format!( + "Date: {}\nSHA256:\n {:x} {} Packages\n", + timestamp, + result, + meta.len() + )) } -impl SimpleAptRepository { - /// Creates a new APT repository object. - pub fn new>(path: P) -> Self { - Self { - path: path.as_ref().to_owned(), - } - } - - /// Returns the `debs` directory. - pub fn directory(&self) -> &Path { - &self.path - } - - /// Returns the path of `Packages` file. - pub fn packages_file(&self) -> PathBuf { - self.path.join("Packages") - } - - /// Returns the path of `Packages` file. - pub fn release_file(&self) -> PathBuf { - self.path.join("Release") - } - - /// Returns the path of `fresh.lock` file. - pub fn refresh_lock_file(&self) -> PathBuf { - self.path.join("fresh.lock") - } +/// Refresh the local repository (Update Packages file) +pub fn refresh_repo(root: &Path) -> Result<()> { + let path = root.join("debs"); + fs::create_dir_all(&path)?; + let mut output = fs::File::create(path.join("Packages"))?; + let entries = scan::collect_all_packages(&path)?; + info!("Scanning {} packages...", entries.len()); + output.write_all(&scan::scan_packages_simple(&entries, &path))?; + println!(); + + let release = generate_release(&path)?; + let mut release_file = fs::File::create(path.join("Release"))?; + release_file.write_all(release.as_bytes())?; + + Ok(()) } -impl SimpleAptRepository { - /// Generates the `Release` file. - pub fn generate_release(&self) -> Result { - let mut f = fs::File::open(self.packages_file())?; - - let mut hasher = Sha256::new(); - std::io::copy(&mut f, &mut hasher)?; - let sha256sum = hex_string(&hasher.finalize()); - - let meta = f.metadata()?; - let timestamp = OffsetDateTime::now_utc().format(&DEB822_DATE)?; - - Ok(format!( - "Date: {}\nSHA256:\n {} {} Packages\n", - timestamp, - sha256sum, - meta.len() - )) - } - - /// Refreshes the repository index, i.e. `Packages` and `Release` file. - pub fn refresh(&self) -> Result<()> { - fs::create_dir_all(self.directory())?; - - let entries = scan::collect_all_packages(self.directory())?; - info!("Scanning {} packages ...", entries.len()); - { - let mut file = fs::File::create(self.packages_file())?; - for chunk in scan::scan_packages_simple(&entries, self.directory())? { - file.write(&chunk)?; - } - } - fs::write(self.release_file(), self.generate_release()?)?; - info!("Refreshed all packages"); - - Ok(()) - } +/// Initialize local repository and add entries to sources.list +pub fn init_repo(repo_root: &Path, rootfs: &Path) -> Result<()> { + // trigger a refresh, since the metadata is probably out of date + refresh_repo(repo_root)?; + fs::create_dir_all(rootfs.join("etc/apt/sources.list.d/"))?; + fs::write( + rootfs.join("etc/apt/sources.list.d/ciel-local.list"), + b"deb [trusted=yes] file:///debs/ /", + )?; + + Ok(()) } -#[cfg(test)] -mod test { - use std::fs; - - use test_log::test; - - use crate::test::TestDir; - - #[test] - fn test_simple_apt_repo_refresh() { - let testdir = TestDir::from("testdata/simple-repo"); - let repo = testdir.apt_repo(); - repo.refresh().unwrap(); - assert_eq!( - fs::read_to_string("testdata/simple-repo/debs/Packages").unwrap(), - fs::read_to_string(testdir.path().join("debs/Packages")).unwrap(), - ) - } +/// Uninitialize the repository +pub fn deinit_repo(rootfs: &Path) -> Result<()> { + Ok(fs::remove_file( + rootfs.join("etc/apt/sources.list.d/ciel-local.list"), + )?) } diff --git a/src/repo/monitor.rs b/src/repo/monitor.rs index 5c45323..0131079 100644 --- a/src/repo/monitor.rs +++ b/src/repo/monitor.rs @@ -1,25 +1,31 @@ +use crate::info; +use anyhow::Result; +use console::style; +use fs3::FileExt; use inotify::{Inotify, WatchMask}; -use log::info; use std::{ - fs::{self, File}, + fs::File, io::{Read, Seek, Write}, ops::{Deref, DerefMut}, path::Path, - sync::mpsc::{self, Receiver, Sender}, + sync::mpsc::Receiver, thread::sleep, time::Duration, }; -use crate::Result; +use super::refresh_repo; -use super::SimpleAptRepository; +const LOCK_FILE: &str = "debs/fresh.lock"; -struct FreshLockGuard(File); +struct FreshLockGuard { + inner: File, +} impl FreshLockGuard { fn new(file: File) -> Result { - fs3::FileExt::lock_exclusive(&file)?; - Ok(Self(file)) + file.lock_exclusive()?; + + Ok(Self { inner: file }) } } @@ -27,57 +33,47 @@ impl Deref for FreshLockGuard { type Target = File; fn deref(&self) -> &Self::Target { - &self.0 + &self.inner } } impl DerefMut for FreshLockGuard { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + &mut self.inner } } impl Drop for FreshLockGuard { fn drop(&mut self) { - fs3::FileExt::unlock(&self.0).unwrap(); + self.inner.unlock().ok(); } } -/// A monitor thread to refresh repository automatically. -pub struct RepositoryRefreshMonitor { - thread: std::thread::JoinHandle>, - stop_handle: Sender<()>, -} - -impl RepositoryRefreshMonitor { - /// Starts a new repository refresh monitor. - pub fn new(repo: SimpleAptRepository) -> Self { - let (tx, rx) = mpsc::channel(); - let thread = std::thread::spawn(move || run_monitor(repo, rx)); - Self { - thread, - stop_handle: tx, - } +fn refresh_once(pool_path: &Path) -> Result<()> { + let lock_file = pool_path.join(LOCK_FILE); + let f = match File::options().read(true).write(true).open(&lock_file) { + Ok(f) => f, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => File::create(&lock_file)?, + Err(e) => return Err(e.into()), + }; + let mut guarded = FreshLockGuard::new(f)?; + let mut buf = [0u8; 1]; + guarded.read_exact(&mut buf)?; + if buf[0] != b'1' { + refresh_repo(pool_path)?; + guarded.rewind()?; + guarded.write_all("1".as_bytes())?; } - /// Stops the monitor. - pub fn stop(self) -> Result<()> { - _ = self.stop_handle.send(()); - self.thread.join().unwrap() - } + Ok(()) } -fn run_monitor(repo: SimpleAptRepository, stop_handle: Receiver<()>) -> Result<()> { +pub fn start_monitor(pool_path: &Path, stop_token: Receiver<()>) -> Result<()> { // ensure lock exists - let lock_path = repo.refresh_lock_file(); + let lock_path = pool_path.join(LOCK_FILE); if !Path::exists(&lock_path) { - info!("Creating fresh lock file at {:?} ...", lock_path); - if let Some(parent) = lock_path.parent() { - if !parent.exists() { - fs::create_dir_all(parent)?; - } - } File::create(&lock_path)?; + info!("Creating lock file at {}...", LOCK_FILE); } let mut inotify = Inotify::init()?; @@ -89,12 +85,9 @@ fn run_monitor(repo: SimpleAptRepository, stop_handle: Receiver<()>) -> Result<( )?; loop { - match stop_handle.try_recv() { - Ok(()) => return Ok(()), - Err(mpsc::TryRecvError::Empty) => {} - Err(mpsc::TryRecvError::Disconnected) => return Ok(()), + if stop_token.try_recv().is_ok() { + return Ok(()); } - sleep(Duration::from_secs(1)); match inotify.read_events(&mut buffer) { Ok(_) => { @@ -102,7 +95,7 @@ fn run_monitor(repo: SimpleAptRepository, stop_handle: Receiver<()>) -> Result<( ignore_next = false; continue; } - refresh_once(&repo)?; + refresh_once(pool_path).ok(); ignore_next = true; } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue, @@ -110,27 +103,3 @@ fn run_monitor(repo: SimpleAptRepository, stop_handle: Receiver<()>) -> Result<( } } } - -fn refresh_once(repo: &SimpleAptRepository) -> Result<()> { - let lock_file = repo.refresh_lock_file(); - let f = match File::options() - .read(true) - .write(true) - .create(true) - .open(&lock_file) - { - Ok(f) => f, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => File::create(&lock_file)?, - Err(e) => return Err(e.into()), - }; - let mut f = FreshLockGuard::new(f)?; - let mut buf = [0u8; 1]; - f.read_exact(&mut buf)?; - if buf[0] != b'1' { - repo.refresh()?; - f.rewind()?; - f.write_all("1".as_bytes())?; - } - - Ok(()) -} diff --git a/src/repo/scan.rs b/src/repo/scan.rs index 9a67e17..71ebd9d 100644 --- a/src/repo/scan.rs +++ b/src/repo/scan.rs @@ -1,100 +1,29 @@ -use core::str; +use crate::error; +use anyhow::{anyhow, Result}; +use ar::Archive as ArArchive; +use console::style; use faster_hex::hex_string; use flate2::read::GzDecoder; -use log::error; use rayon::prelude::*; use sha2::{Digest, Sha256}; +use std::io::SeekFrom; use std::{ fs::File, - io::{Read, Seek, SeekFrom}, - path::{Path, PathBuf}, + io::{Read, Seek, Write}, + path::Path, }; -use walkdir::WalkDir; +use tar::Archive as TarArchive; +use walkdir::{DirEntry, WalkDir}; use xz2::read::XzDecoder; -#[non_exhaustive] -#[derive(thiserror::Error, Debug)] -pub enum ScanError { - #[error("I/O error: {0}")] - IoError(#[from] std::io::Error), - #[error(transparent)] - WalkDirError(#[from] walkdir::Error), - #[error(transparent)] - StripPrefixError(#[from] std::path::StripPrefixError), - - #[error("Unknown control.tar compression type: {0}")] - UnknownControlTarType(String), - #[error("control.tar not found")] - MissingControlTar, - #[error("control file not found")] - MissingControlFile, -} - -pub type Result = std::result::Result; - -pub(crate) fn collect_all_packages>(path: P) -> crate::Result> { - let mut files = Vec::new(); - for entry in WalkDir::new(path.as_ref()) { - let entry = entry?; - if entry - .file_name() - .to_str() - .map(|s| s.ends_with(".deb")) - .unwrap_or(false) - { - files.push(entry.into_path()); - } - } - Ok(files) -} - -pub(crate) fn scan_packages_simple( - entries: &[PathBuf], - root: &Path, -) -> crate::Result>> { - entries - .par_iter() - .map(|path| -> crate::Result> { - scan_single_deb_simple(path.as_path(), root) - .map_err(|err| crate::Error::DebScanError(path.to_owned(), err)) - }) - .collect() -} - -fn scan_single_deb_simple>(path: P, root: P) -> Result> { - let mut f = File::open(path.as_ref())?; - - let mut hasher = Sha256::new(); - std::io::copy(&mut f, &mut hasher)?; - let sha256sum = hex_string(&hasher.finalize()); - - let actual_size = f.stream_position()?; - f.seek(SeekFrom::Start(0))?; - - let mut control = open_deb(f)?; - control.reserve(128); - if control.ends_with(&b"\n\n"[..]) { - control.pop(); - } - let rel_path = path.as_ref().strip_prefix(root)?; - control.extend(format!("Size: {}\n", actual_size).as_bytes()); - control.extend(format!("Filename: {}\n", rel_path.to_string_lossy()).as_bytes()); - control.extend(b"SHA256: "); - control.extend(sha256sum.as_bytes()); - control.extend(b"\n\n"); - - Ok(control) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum TarCompressionType { +enum TarFormat { Xzip, Gzip, Zstd, } fn collect_control(reader: R) -> Result> { - let mut tar = tar::Archive::new(reader); + let mut tar = TarArchive::new(reader); for entry in tar.entries()? { let mut entry = entry?; if entry.path_bytes().as_ref() == &b"./control"[..] { @@ -103,11 +32,32 @@ fn collect_control(reader: R) -> Result> { return Ok(buf); } } - Err(ScanError::MissingControlFile) + + Err(anyhow!("Could not read control file")) +} + +fn open_compressed_control(reader: R, format: &TarFormat) -> Result> { + match format { + TarFormat::Xzip => collect_control(XzDecoder::new(reader)), + TarFormat::Gzip => collect_control(GzDecoder::new(reader)), + TarFormat::Zstd => collect_control(zstd::stream::read::Decoder::new(reader)?), + } } -fn open_deb(reader: R) -> Result> { - let mut deb = ar::Archive::new(reader); +fn determine_format(format: &[u8]) -> Result { + if format.ends_with(b".xz") { + Ok(TarFormat::Xzip) + } else if format.ends_with(b".gz") { + Ok(TarFormat::Gzip) + } else if format.ends_with(b".zst") { + Ok(TarFormat::Zstd) + } else { + Err(anyhow!("Unknown format: {:?}", format)) + } +} + +fn open_deb_simple(reader: R) -> Result> { + let mut deb = ArArchive::new(reader); while let Some(entry) = deb.next_entry() { if entry.is_err() { continue; @@ -115,126 +65,79 @@ fn open_deb(reader: R) -> Result> { let entry = entry?; let filename = entry.header().identifier(); if filename.starts_with(b"control.tar") { - let format = determine_compression(filename)?; - let control = open_compressed_control(entry, format)?; + let format = determine_format(filename)?; + let control = open_compressed_control(entry, &format)?; return Ok(control); } } - Err(ScanError::MissingControlTar) -} -fn open_compressed_control(reader: R, format: TarCompressionType) -> Result> { - match format { - TarCompressionType::Xzip => collect_control(XzDecoder::new(reader)), - TarCompressionType::Gzip => collect_control(GzDecoder::new(reader)), - TarCompressionType::Zstd => collect_control(zstd::stream::read::Decoder::new(reader)?), - } + Err(anyhow!("data archive not found or format unsupported")) } -fn determine_compression(format: &[u8]) -> Result { - if format.ends_with(b".xz") { - Ok(TarCompressionType::Xzip) - } else if format.ends_with(b".gz") { - Ok(TarCompressionType::Gzip) - } else if format.ends_with(b".zst") { - Ok(TarCompressionType::Zstd) - } else { - Err(ScanError::UnknownControlTarType( - str::from_utf8(format).unwrap().to_string(), - )) +fn scan_single_deb_simple>(path: P, root: P) -> Result> { + let mut f = File::open(path.as_ref())?; + let sha256 = sha256sum(&mut f)?; + let actual_size = f.stream_position()?; + f.seek(SeekFrom::Start(0))?; + let mut control = open_deb_simple(f)?; + control.reserve(128); + if control.ends_with(&b"\n\n"[..]) { + control.pop(); } + let rel_path = path.as_ref().strip_prefix(root)?; + control.extend(format!("Size: {}\n", actual_size).as_bytes()); + control.extend(format!("Filename: {}\n", rel_path.to_string_lossy()).as_bytes()); + control.extend(b"SHA256: "); + control.extend(sha256.as_bytes()); + control.extend(b"\n\n"); + + Ok(control) } -#[cfg(test)] -mod test { - use test_log::test; +/// Calculate the Sha256 checksum of the given stream +pub fn sha256sum(mut reader: R) -> Result { + let mut hasher = Sha256::new(); + std::io::copy(&mut reader, &mut hasher)?; - use crate::{ - repo::scan::{collect_all_packages, scan_packages_simple, scan_single_deb_simple}, - test::TestDir, - }; + Ok(hex_string(&hasher.finalize())) +} - #[test] - fn test_collect_all_packages() { - let testdir = TestDir::from("testdata/simple-repo"); - assert_eq!( - collect_all_packages(testdir.path().join("debs")).unwrap(), - vec![ - testdir - .path() - .join("debs/a/aosc-os-feature-data_20241017.1-0_noarch.deb") - ] - ); - } +#[inline] +fn is_tarball(entry: &DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.ends_with(".deb")) + .unwrap_or(false) +} - #[test] - fn test_scan_single_deb_simple() { - let testdir = TestDir::from("testdata/simple-repo"); - assert_eq!( - String::from_utf8( - scan_single_deb_simple( - testdir - .path() - .join("debs/a/aosc-os-feature-data_20241017.1-0_noarch.deb"), - testdir.path().join("debs") - ) - .unwrap() - ) - .unwrap(), - r##"Package: aosc-os-feature-data -Version: 20241017.1 -Architecture: all -Section: misc -Maintainer: AOSC OS Maintainers -Installed-Size: 56 -Description: Data defining key AOSC OS features -Description-md5: 248f104b2025bbfc686d24bee09cb14c -Essential: no -X-AOSC-ACBS-Version: 20241023 -X-AOSC-Commit: 9c93f94783 -X-AOSC-Packager: AOSC OS Maintainers -X-AOSC-Autobuild4-Version: 4.3.27 -Size: 1838 -Filename: a/aosc-os-feature-data_20241017.1-0_noarch.deb -SHA256: dd386883fa246cc50826cced5df4353b64a490d3f0f487e2d8764b4d7d00151e +pub fn scan_packages_simple(entries: &[DirEntry], root: &Path) -> Vec { + entries + .par_iter() + .map(|entry| -> Vec { + let path = entry.path(); + print!("."); + std::io::stderr().flush().ok(); + match scan_single_deb_simple(path, root) { + Ok(entry) => entry, + Err(err) => { + error!("{:?}", err); + Vec::new() + } + } + }) + .flatten() + .collect() +} -"## - ); +pub fn collect_all_packages>(path: P) -> Result> { + let mut files = Vec::new(); + for entry in WalkDir::new(path.as_ref()) { + let entry = entry?; + if is_tarball(&entry) { + files.push(entry); + } } - #[test] - fn test_scan_packages_simple() { - let testdir = TestDir::from("testdata/simple-repo"); - assert_eq!( - String::from_utf8( - scan_packages_simple( - &[testdir - .path() - .join("debs/a/aosc-os-feature-data_20241017.1-0_noarch.deb")], - &testdir.path().join("debs") - ) - .unwrap() - .concat() - ) - .unwrap(), - r##"Package: aosc-os-feature-data -Version: 20241017.1 -Architecture: all -Section: misc -Maintainer: AOSC OS Maintainers -Installed-Size: 56 -Description: Data defining key AOSC OS features -Description-md5: 248f104b2025bbfc686d24bee09cb14c -Essential: no -X-AOSC-ACBS-Version: 20241023 -X-AOSC-Commit: 9c93f94783 -X-AOSC-Packager: AOSC OS Maintainers -X-AOSC-Autobuild4-Version: 4.3.27 -Size: 1838 -Filename: a/aosc-os-feature-data_20241017.1-0_noarch.deb -SHA256: dd386883fa246cc50826cced5df4353b64a490d3f0f487e2d8764b4d7d00151e - -"## - ); - } + Ok(files) } diff --git a/src/workspace.rs b/src/workspace.rs deleted file mode 100644 index c9c29b7..0000000 --- a/src/workspace.rs +++ /dev/null @@ -1,808 +0,0 @@ -use std::{ - fmt::Debug, - fs, - path::{Path, PathBuf}, - sync::Arc, - sync::RwLock, -}; - -use log::info; -use rand::Rng; -use serde::{Deserialize, Serialize}; - -use crate::{ - container::OwnedContainer, instance::Instance, Container, Error, InstanceConfig, Result, -}; - -/// A Ciel workspace. -/// -/// A workspace is a directory containing the following things: -/// - A workspace configuration (`.ciel/data/config.toml`) -/// - A base system for all build containers (`.ciel/container/dist`) -/// - Some instances ([Instance]) -/// - (optional) Some OUTPUT directories for output deb files. -/// - (optional) A CACHE directory for caching source tarballs. -/// - (optional) A TREE directory for the default abbs tree. -/// -/// Workspaces may have their base system loaded or unloaded -/// (i.e. there is no base system) -/// -/// ```rust,no_run -/// use ciel::Workspace; -/// -/// let workspace = Workspace::current_dir().unwrap(); -/// dbg!(workspace.instances().unwrap().is_empty()); -/// ``` -#[derive(Clone)] -pub struct Workspace { - path: Arc, - config: Arc>, -} - -impl Workspace { - /// The current version of workspace format. - pub const CURRENT_VERSION: usize = 3; - - pub(crate) const CIEL_DIR: &str = ".ciel"; - pub(crate) const DATA_DIR: &str = ".ciel/data"; - pub(crate) const VERSION_PATH: &str = ".ciel/version"; - pub(crate) const DIST_DIR: &str = ".ciel/container/dist"; - pub(crate) const INSTANCES_DIR: &str = ".ciel/container/instances"; - - /// Begins an existing workspace at the given path. - /// - /// This does not initialize a new workspace if not. - /// To start a fully new workspace, see [Self::init]. - /// - /// If the workspace is a legacy workspace (version 2), a default - /// workspace configuration will be saved and the workspace will be - /// upgraded to the current version. - pub fn new>(path: P) -> Result { - let path = path.as_ref(); - - if !path.join(Self::CIEL_DIR).is_dir() { - return Err(Error::BrokenWorkspace); - } - if !path.join(Self::VERSION_PATH).is_file() { - return Err(Error::BrokenWorkspace); - } - - let version = fs::read_to_string(path.join(".ciel/version"))? - .trim() - .parse::() - .map_err(|_| Error::NotAWorkspace)?; - match version { - Self::CURRENT_VERSION => {} - 2 => { - fs::create_dir_all(path.join(Self::DATA_DIR))?; - fs::write( - path.join(WorkspaceConfig::PATH), - WorkspaceConfig::default().serialize()?, - )?; - fs::write( - path.join(Self::VERSION_PATH), - Self::CURRENT_VERSION.to_string(), - )?; - } - _ => return Err(Error::UnsupportedWorkspaceVersion(version)), - } - - for dir in [Self::DATA_DIR, Self::DIST_DIR, Self::INSTANCES_DIR] { - if !path.join(dir).is_dir() { - return Err(Error::BrokenWorkspace); - } - } - for dir in [WorkspaceConfig::PATH] { - if !path.join(dir).is_file() { - return Err(Error::BrokenWorkspace); - } - } - - let config = WorkspaceConfig::load(path.join(WorkspaceConfig::PATH))?; - - Ok(Self { - path: Arc::new(path.into()), - config: Arc::new(config.into()), - }) - } - - /// Begins an existing workspace at the current directory. - /// - /// This is equivalent to `Workspace::new(std::env::current_dir()?)`. - pub fn current_dir() -> Result { - Self::new(std::env::current_dir()?) - } - - /// Initializes a fully new workspace at the given directory, - /// with the given configuration. - /// - /// The newly initialized workspace has its base system unloaded. - /// To load a base system, extract files into [Self::system_rootfs]. - pub fn init>(path: P, config: WorkspaceConfig) -> Result { - let path = path.as_ref(); - - if path.join(".ciel").exists() { - return Err(Error::WorkspaceAlreadyExists); - } - - info!("Initializing new CIEL! workspace at {:?}", path); - - fs::create_dir_all(path.join(Self::CIEL_DIR))?; - fs::create_dir_all(path.join(Self::DATA_DIR))?; - fs::create_dir_all(path.join(Self::DIST_DIR))?; - fs::create_dir_all(path.join(Self::INSTANCES_DIR))?; - fs::write( - path.join(Self::VERSION_PATH), - Self::CURRENT_VERSION.to_string(), - )?; - fs::write(path.join(WorkspaceConfig::PATH), config.serialize()?)?; - - Ok(Self { - path: Arc::new(path.into()), - config: Arc::new(config.into()), - }) - } - - /// Gets the directory, at which this workspace is placed, as [Path]. - pub fn directory(&self) -> &Path { - &self.path - } - - /// Gets the workspace configuration. - pub fn config(&self) -> WorkspaceConfig { - self.config.read().unwrap().to_owned() - } - - /// Modifies the workspace configuration after validation. - pub fn set_config(&self, config: WorkspaceConfig) -> Result<()> { - config.validate()?; - fs::write( - self.directory().join(WorkspaceConfig::PATH), - config.serialize()?, - )?; - *self.config.write()? = config; - Ok(()) - } - - /// Lists all existing instances. - pub fn instances(&self) -> Result> { - let mut instances = vec![]; - for entry in self.directory().join(Self::INSTANCES_DIR).read_dir()? { - let entry = entry?; - if !entry.file_type()?.is_dir() { - continue; - } - if let Some(name) = entry.file_name().to_str() { - instances.push(Instance::new(self.clone(), name.to_string())?); - } else { - return Err(Error::InvalidInstanceName(entry.file_name())); - } - } - Ok(instances) - } - - /// Gets an existing instance. - pub fn instance>(&self, name: S) -> Result { - Instance::new(self.clone(), name.as_ref().to_string()) - } - - /// Creates a new instance. - pub fn add_instance>(&self, name: S, config: InstanceConfig) -> Result { - let name = name.as_ref(); - - let instance_dir = self.directory().join(Workspace::INSTANCES_DIR).join(name); - fs::create_dir_all(&instance_dir)?; - fs::write(instance_dir.join(InstanceConfig::PATH), config.serialize()?)?; - info!("{}: instance created", name); - - self.instance(name) - } - - /// Returns the rootfs path of the base system. - pub fn system_rootfs(&self) -> PathBuf { - self.directory().join(Self::DIST_DIR) - } - - /// Returns if the base system has been loaded. - pub fn is_system_loaded(&self) -> bool { - self.system_rootfs() - .read_dir() - .map(|mut r| r.next().is_some()) - .unwrap_or_default() - } - - /// Commits changes in a container into the base system. - /// - /// Caller must ensure that only the container to commit is opened. - /// Other containers will be locked and rollbacked during the commit. - pub fn commit>(&self, container: C) -> Result<()> { - let container = container.as_ref(); - container.stop(true)?; - let mut locks = vec![]; - for inst in self.instances()? { - if &inst != container.instance() { - let inst = inst.open()?; - inst.rollback()?; - locks.push(inst); - } - } - container.overlay_manager().commit()?; - container.rollback()?; - Ok(()) - } - - /// Destroies the workspace, removing all Ciel files, except for - /// the abbs tree, caches and outputs. - pub fn destroy(self) -> Result<()> { - for inst in self.instances()? { - let inst = inst.open()?; - inst.stop(true)?; - inst.overlay_manager().rollback()?; - } - fs::remove_dir_all(self.directory().join(".ciel"))?; - Ok(()) - } - - /// Creates a ephemeral owned container with the given prefix. - /// - /// The name of ephemeral containers are formatted as: `$prefix-$rand`. - /// - /// These ephemeral containers are useful for one-time tasks, such as updating - /// the base system. - pub fn ephemeral_container( - &self, - prefix: &str, - config: InstanceConfig, - ) -> Result { - let name = format!("{}-{:08x}", prefix, rand::thread_rng().r#gen::()); - Ok(self.add_instance(name, config)?.open()?.into()) - } - - /// Returns the output directory of the workspace. - /// - /// See [Container::output_directory]. - pub fn output_directory(&self) -> PathBuf { - let name = if self.config().branch_exclusive_output { - let head = if let Ok(repo) = git2::Repository::open(self.directory().join("TREE")) { - repo.head() - .ok() - .and_then(|head| head.shorthand().map(|s| s.to_string())) - .unwrap_or_else(|| "HEAD".to_string()) - } else { - "HEAD".to_string() - }; - format!("OUTPUT-{}", head) - } else { - "OUTPUT".to_string() - }; - self.directory().join(name).join("debs") - } -} - -impl Debug for Workspace { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("CIEL workspace `{:?}`", self.directory())) - } -} - -impl TryFrom<&Path> for Workspace { - type Error = crate::Error; - - fn try_from(value: &Path) -> std::result::Result { - Self::new(value) - } -} - -impl From for PathBuf { - fn from(value: Workspace) -> Self { - value.directory().to_owned() - } -} - -impl PartialEq for Workspace { - fn eq(&self, other: &Self) -> bool { - self.path == other.path - } -} - -/// A Ciel workspace configuration. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct WorkspaceConfig { - version: usize, - /// The maintainer information, for example, `Bot ` - pub maintainer: String, - /// Whether DNSSEC should be allowed in containers. - #[serde(default)] - pub dnssec: bool, - - // The old version of ciel-rs uses `apt_sources`, which is kept for compatibility. - // This is converted into [extra_apt_repos] when loaded. - #[serde(alias = "apt_sources", default)] - apt_sources: Option, - /// Extra APT repositories to use. - #[serde(default)] - pub extra_apt_repos: Vec, - /// Whether local repository (the output directory) should be enabled in containers. - #[serde(alias = "local_repo", default)] - pub use_local_repo: bool, - /// Whether output directories should be branch-exclusive . - /// - /// This means using `OUTPUT-(branch)` instead of `OUTPUT` for outputs. - #[serde(default)] - pub branch_exclusive_output: bool, - - /// Whether to cache APT packages. - #[serde(default)] - pub no_cache_packages: bool, - /// Whether to cache sources. - #[serde(alias = "local_sources", default)] - pub cache_sources: bool, - - /// Extra options for systemd-nspawn - #[serde(alias = "nspawn-extra-options", default)] - pub extra_nspawn_options: Vec, - - /// Whether to mount the container filesystem as volatile - #[serde(default)] - pub volatile_mount: bool, - - /// Whether to use APT instead of oma. - /// - /// This is enabled by default on RISC-V hosts, because oma may run into - /// random lock-ups on RISC-V. - #[serde(alias = "force_use_apt", default = "WorkspaceConfig::default_use_apt")] - pub use_apt: bool, -} - -impl WorkspaceConfig { - const fn default_use_apt() -> bool { - cfg!(target_arch = "riscv64") - } -} - -impl Default for WorkspaceConfig { - fn default() -> Self { - Self { - version: Self::CURRENT_VERSION, - maintainer: "Bot ".to_string(), - dnssec: false, - apt_sources: None, - extra_apt_repos: vec![], - use_local_repo: true, - branch_exclusive_output: true, - no_cache_packages: false, - cache_sources: true, - extra_nspawn_options: vec![], - volatile_mount: false, - use_apt: Self::default_use_apt(), - } - } -} - -impl WorkspaceConfig { - /// The default path for workspace configuration. - pub const PATH: &str = ".ciel/data/config.toml"; - - /// The current version of workspace configuration format. - pub const CURRENT_VERSION: usize = 3; - - /// Loads a workspace configuration from a given file path. - pub fn load>(path: P) -> Result { - let path = path.as_ref().to_path_buf(); - if path.exists() { - fs::read_to_string(&path)?.as_str().try_into() - } else { - Err(Error::ConfigNotFound(path)) - } - } - - /// Validate the configuration. - /// - /// This checks: - /// - Invalid maintainer string - pub fn validate(&self) -> Result<()> { - Self::validate_maintainer(&self.maintainer)?; - Ok(()) - } - - /// Validates a maintainer information string. - /// - /// This ensures the string has a valid maintainer name and email address. - pub fn validate_maintainer(maintainer: &str) -> Result<()> { - let mut lt = false; // "<" - let mut gt = false; // ">" - let mut at = false; // "@" - let mut name = false; - let mut nbsp = false; // space - // A simple FSM to match the states - for c in maintainer.as_bytes() { - match *c { - b'<' => { - if !nbsp { - return Err(Error::MaintainerNameNeeded); - } - lt = true; - } - b'>' => { - if !lt { - return Err(Error::InvalidMaintainerInfo); - } - gt = true; - } - b'@' => { - if !lt || gt { - return Err(Error::InvalidMaintainerInfo); - } - at = true; - } - b' ' | b'\t' => { - if !name { - return Err(Error::MaintainerNameNeeded); - } - nbsp = true; - } - _ => { - if !nbsp { - name = true; - continue; - } - } - } - } - - if name && gt && lt && at { - return Ok(()); - } - - Err(Error::InvalidMaintainerInfo) - } - - /// Deserializes a workspace configuration TOML. - pub fn parse(config: &str) -> Result { - let mut config = toml::from_str::(config)?; - - // Convert old `apt_sources` into `extra_apt_repos` - if let Some(sources) = config.apt_sources.take() { - config.extra_apt_repos.extend( - sources - .lines() - .map(|line| line.trim()) - .filter(|line| !line.is_empty()) - .filter(|line| { - !line.eq_ignore_ascii_case("deb https://repo.aosc.io/debs/ stable main") - }) - .map(|line| line.to_string()), - ); - } - - Ok(config) - } - - /// Serializes a workspace configuration into TOML. - pub fn serialize(&self) -> Result { - Ok(toml::to_string_pretty(&self)?) - } -} - -impl TryFrom<&str> for WorkspaceConfig { - type Error = crate::Error; - - fn try_from(value: &str) -> std::result::Result { - Self::parse(value) - } -} - -impl TryFrom<&WorkspaceConfig> for String { - type Error = crate::Error; - - fn try_from(value: &WorkspaceConfig) -> std::result::Result { - value.serialize() - } -} - -#[cfg(test)] -mod test { - use std::fs; - use test_log::test; - - use crate::{ - test::{is_root, TestDir}, - ContainerState, Error, InstanceConfig, - }; - - use super::WorkspaceConfig; - - #[test] - fn test_config() { - let config = WorkspaceConfig::default(); - let serialized = config.serialize().unwrap(); - assert_eq!( - serialized, - r##"version = 3 -maintainer = "Bot " -dnssec = false -extra-apt-repos = [] -use-local-repo = true -branch-exclusive-output = true -no-cache-packages = false -cache-sources = true -extra-nspawn-options = [] -volatile-mount = false -use-apt = false -"## - ); - assert_eq!( - WorkspaceConfig::try_from(serialized.as_str()).unwrap(), - config - ); - } - - #[test] - fn test_config_migration() { - assert_eq!( - WorkspaceConfig::parse( - r##" -version = 3 -maintainer = "AOSC OS Maintainers " -dnssec = false -apt_sources = "deb https://repo.aosc.io/debs/ stable main" -local_repo = true -local_sources = true -branch-exclusive-output = true -volatile-mount = false -nspawn-extra-options = ["-E", "NO_COLOR=1"] -"##, - ) - .unwrap(), - WorkspaceConfig { - version: 3, - maintainer: "AOSC OS Maintainers ".to_string(), - dnssec: false, - apt_sources: None, - extra_apt_repos: vec![], - use_local_repo: true, - branch_exclusive_output: true, - cache_sources: true, - extra_nspawn_options: vec!["-E".to_string(), "NO_COLOR=1".to_string()], - volatile_mount: false, - use_apt: false, - ..Default::default() - } - ); - - assert_eq!( - WorkspaceConfig::parse( - r##" -version = 3 -maintainer = "AOSC OS Maintainers " -dnssec = false -apt_sources = "deb https://repo.aosc.io/debs/ stable main\ndeb file:///test/ test test" -local_repo = true -local_sources = true -nspawn-extra-options = [] -branch-exclusive-output = true -volatile-mount = false -"##, - ) - .unwrap(), - WorkspaceConfig { - version: 3, - maintainer: "AOSC OS Maintainers ".to_string(), - dnssec: false, - apt_sources: None, - extra_apt_repos: vec!["deb file:///test/ test test".to_string()], - use_local_repo: true, - branch_exclusive_output: true, - cache_sources: true, - extra_nspawn_options: vec![], - volatile_mount: false, - use_apt: false, - ..Default::default() - } - ); - } - - #[test] - fn test_validate_maintainer() { - assert!(matches!( - WorkspaceConfig::validate_maintainer("test "), - Ok(()) - )); - assert!(matches!( - WorkspaceConfig::validate_maintainer("test "), - Err(Error::MaintainerNameNeeded) - )); - assert!(matches!( - WorkspaceConfig::validate_maintainer(" "), - Err(Error::MaintainerNameNeeded) - )); - } - - #[test] - fn test_workspace_init() { - let testdir = TestDir::new(); - let ws = testdir.init_workspace(WorkspaceConfig::default()).unwrap(); - dbg!(&ws); - assert!(!ws.is_system_loaded()); - assert!(ws.config().extra_apt_repos.is_empty()); - fs::write(ws.directory().join(".ciel/container/dist/init"), "").unwrap(); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - assert!(ws.is_system_loaded()); - assert!(ws.instances().unwrap().is_empty()); - } - - #[test] - fn test_workspace_migration_v3() { - // migration from Ciel <= 3.6.0 - let testdir = TestDir::from("testdata/old-workspace"); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - assert!(ws.is_system_loaded()); - assert_eq!( - ws.config().extra_apt_repos, - vec!["deb file:///test/ test test".to_string(),] - ); - assert!(ws.config().branch_exclusive_output); - } - - #[test] - fn test_workspace_migration_v2() { - // migration from Ciel 2.x.x - let testdir = TestDir::from("testdata/v2-workspace"); - let ws = testdir.workspace().unwrap(); - dbg!(&ws); - assert!(ws.is_system_loaded()); - assert!(ws.config().extra_apt_repos.is_empty()); - assert!(ws.config().branch_exclusive_output); - } - - #[test] - fn test_incompatible_workspace() { - let testdir = TestDir::from("testdata/incompat-ws-version"); - assert!(matches!( - testdir.workspace(), - Err(Error::UnsupportedWorkspaceVersion(0)) - )); - } - - #[test] - fn test_broken_workspace() { - let testdir = TestDir::from("testdata/broken-workspace"); - assert!(matches!(testdir.workspace(), Err(Error::BrokenWorkspace))); - } - - #[test] - fn test_workspace_instances() { - let testdir = TestDir::from("testdata/simple-workspace"); - let workspace = testdir.workspace().unwrap(); - dbg!(&workspace); - assert_eq!( - workspace - .instances() - .unwrap() - .iter() - .map(|i| i.name().to_owned()) - .collect::>(), - vec!["test".to_string(), "tmpfs".to_string()] - ); - let instance = workspace.instance("test").unwrap(); - dbg!(&instance); - assert_eq!(instance.name(), "test"); - } - - #[test] - fn test_workspace_add_instance() { - let testdir = TestDir::from("testdata/simple-workspace"); - let workspace = testdir.workspace().unwrap(); - dbg!(&workspace); - assert_eq!( - workspace - .instances() - .unwrap() - .iter() - .map(|i| i.name().to_owned()) - .collect::>(), - vec!["test".to_string(), "tmpfs".to_string()] - ); - let instance = workspace - .add_instance("a", InstanceConfig::default()) - .unwrap(); - dbg!(&instance); - assert_eq!(instance.name(), "a"); - assert_eq!( - workspace - .instances() - .unwrap() - .iter() - .map(|i| i.name().to_owned()) - .collect::>(), - vec!["test".to_string(), "tmpfs".to_string(), "a".to_string()] - ); - let instance = workspace.instance("a").unwrap(); - dbg!(&instance); - let container = instance.open().unwrap(); - dbg!(&container); - assert_eq!(container.state().unwrap(), ContainerState::Down); - } - - #[test] - fn test_workspace_commit() { - let testdir = TestDir::from("testdata/simple-workspace"); - let workspace = testdir.workspace().unwrap(); - dbg!(&workspace); - let instance = workspace.instance("test").unwrap(); - dbg!(&instance); - let container = instance.open().unwrap(); - dbg!(&container); - assert_eq!(container.state().unwrap(), ContainerState::Down); - assert!(!testdir.path().join(".ciel/container/dist/a").exists()); - if !is_root() { - return; - } - container.overlay_manager().mount().unwrap(); - assert!(container.overlay_manager().is_mounted().unwrap()); - fs::write(testdir.path().join("test/a"), "test").unwrap(); - workspace.commit(&container).unwrap(); - assert!(!container.overlay_manager().is_mounted().unwrap()); - assert_eq!( - fs::read_to_string(testdir.path().join(".ciel/container/dist/a")).unwrap(), - "test" - ); - } - - #[test] - fn test_workspace_commit_tmpfs() { - let testdir = TestDir::from("testdata/simple-workspace"); - let workspace = testdir.workspace().unwrap(); - dbg!(&workspace); - let instance = workspace.instance("tmpfs").unwrap(); - dbg!(&instance); - let container = instance.open().unwrap(); - dbg!(&container); - assert_eq!(container.state().unwrap(), ContainerState::Down); - assert!(!testdir.path().join(".ciel/container/dist/a").exists()); - if !is_root() { - return; - } - container.overlay_manager().mount().unwrap(); - assert!(container.overlay_manager().is_mounted().unwrap()); - fs::write(testdir.path().join("tmpfs/a"), "test").unwrap(); - workspace.commit(&container).unwrap(); - assert!(!container.overlay_manager().is_mounted().unwrap()); - assert_eq!( - fs::read_to_string(testdir.path().join(".ciel/container/dist/a")).unwrap(), - "test" - ); - } - - #[test] - fn test_workspace_destroy() { - let testdir = TestDir::from("testdata/simple-workspace"); - let workspace = testdir.workspace().unwrap(); - dbg!(&workspace); - workspace.destroy().unwrap(); - assert!(!testdir.path().join(".ciel").exists()); - assert!(testdir.path().join("TREE").exists()); - } - - #[test] - fn test_workspace_ephemeral_container() { - let testdir = TestDir::from("testdata/simple-workspace"); - let workspace = testdir.workspace().unwrap(); - dbg!(&workspace); - let cont = workspace - .ephemeral_container("test", InstanceConfig::default()) - .unwrap(); - dbg!(&cont); - assert!(cont.as_ns_name().starts_with("test-")); - assert_eq!(workspace.instances().unwrap().len(), 3); - drop(cont); - assert_eq!(workspace.instances().unwrap().len(), 2); - } -} diff --git a/testdata/broken-workspace/.ciel/data/config.toml b/testdata/broken-workspace/.ciel/data/config.toml deleted file mode 100644 index 4e55f73..0000000 --- a/testdata/broken-workspace/.ciel/data/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -version = 3 -maintainer = "Bot " -dnssec = false -apt_sources = "deb https://repo.aosc.io/debs/ stable main" -local_repo = true -local_sources = true -nspawn-extra-options = [] -branch-exclusive-output = true -volatile-mount = false -force_use_apt = false diff --git a/testdata/broken-workspace/.ciel/version b/testdata/broken-workspace/.ciel/version deleted file mode 100644 index e440e5c..0000000 --- a/testdata/broken-workspace/.ciel/version +++ /dev/null @@ -1 +0,0 @@ -3 \ No newline at end of file diff --git a/testdata/incompat-ws-version/.ciel/data/config.toml b/testdata/incompat-ws-version/.ciel/data/config.toml deleted file mode 100644 index 4e55f73..0000000 --- a/testdata/incompat-ws-version/.ciel/data/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -version = 3 -maintainer = "Bot " -dnssec = false -apt_sources = "deb https://repo.aosc.io/debs/ stable main" -local_repo = true -local_sources = true -nspawn-extra-options = [] -branch-exclusive-output = true -volatile-mount = false -force_use_apt = false diff --git a/testdata/incompat-ws-version/.ciel/version b/testdata/incompat-ws-version/.ciel/version deleted file mode 100644 index c227083..0000000 --- a/testdata/incompat-ws-version/.ciel/version +++ /dev/null @@ -1 +0,0 @@ -0 \ No newline at end of file diff --git a/testdata/old-workspace/.ciel/container/dist/.gitkeep b/testdata/old-workspace/.ciel/container/dist/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/testdata/old-workspace/.ciel/container/instances/test/layers/diff/.gitkeep b/testdata/old-workspace/.ciel/container/instances/test/layers/diff/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/acbs/forest.conf b/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/acbs/forest.conf deleted file mode 100644 index 4cd6827..0000000 --- a/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/acbs/forest.conf +++ /dev/null @@ -1,2 +0,0 @@ -[default] -location = /tree/ diff --git a/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/apt/sources.list b/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/apt/sources.list deleted file mode 100644 index fe70b97..0000000 --- a/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/apt/sources.list +++ /dev/null @@ -1 +0,0 @@ -deb https://repo.aosc.io/debs/ stable main \ No newline at end of file diff --git a/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/autobuild/ab4cfg.sh b/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/autobuild/ab4cfg.sh deleted file mode 100644 index 7541c4d..0000000 --- a/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/autobuild/ab4cfg.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -ABMPM=dpkg -ABAPMS= -ABINSTALL=dpkg -MTER="Bot " \ No newline at end of file diff --git a/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/systemd/resolved.conf b/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/systemd/resolved.conf deleted file mode 100644 index d43d54a..0000000 --- a/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/systemd/resolved.conf +++ /dev/null @@ -1,2 +0,0 @@ -[Resolve] -DNSSEC=no diff --git a/testdata/old-workspace/.ciel/data/config.toml b/testdata/old-workspace/.ciel/data/config.toml deleted file mode 100644 index 9c4a314..0000000 --- a/testdata/old-workspace/.ciel/data/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -version = 3 -maintainer = "Bot " -dnssec = false -apt_sources = "deb https://repo.aosc.io/debs/ stable main\ndeb file:///test/ test test" -local_repo = true -local_sources = true -nspawn-extra-options = [] -branch-exclusive-output = true -volatile-mount = false -force_use_apt = false diff --git a/testdata/old-workspace/.ciel/version b/testdata/old-workspace/.ciel/version deleted file mode 100644 index e440e5c..0000000 --- a/testdata/old-workspace/.ciel/version +++ /dev/null @@ -1 +0,0 @@ -3 \ No newline at end of file diff --git a/testdata/simple-repo/debs/Packages b/testdata/simple-repo/debs/Packages deleted file mode 100644 index be3c2d7..0000000 --- a/testdata/simple-repo/debs/Packages +++ /dev/null @@ -1,17 +0,0 @@ -Package: aosc-os-feature-data -Version: 20241017.1 -Architecture: all -Section: misc -Maintainer: AOSC OS Maintainers -Installed-Size: 56 -Description: Data defining key AOSC OS features -Description-md5: 248f104b2025bbfc686d24bee09cb14c -Essential: no -X-AOSC-ACBS-Version: 20241023 -X-AOSC-Commit: 9c93f94783 -X-AOSC-Packager: AOSC OS Maintainers -X-AOSC-Autobuild4-Version: 4.3.27 -Size: 1838 -Filename: a/aosc-os-feature-data_20241017.1-0_noarch.deb -SHA256: dd386883fa246cc50826cced5df4353b64a490d3f0f487e2d8764b4d7d00151e - diff --git a/testdata/simple-repo/debs/Release b/testdata/simple-repo/debs/Release deleted file mode 100644 index d101709..0000000 --- a/testdata/simple-repo/debs/Release +++ /dev/null @@ -1,3 +0,0 @@ -Date: Sat, 21 Dec 2024 13:54:04 +0000 -SHA256: - 210a9a412bcbf11772d7b5ae5406a788bc8faf3c1c1266e31ef31bd4af82a968 558 Packages diff --git a/testdata/simple-workspace/.ciel/container/dist/etc/os-release b/testdata/simple-workspace/.ciel/container/dist/etc/os-release deleted file mode 100644 index 613fe22..0000000 --- a/testdata/simple-workspace/.ciel/container/dist/etc/os-release +++ /dev/null @@ -1,10 +0,0 @@ -PRETTY_NAME="mysterious OS" -NAME="CIEL OS" -VERSION_ID="12.0.1" -VERSION="12.0.1 (localhost)" -BUILD_ID="20241210" -ID=aosc -ANSI_COLOR="1;36" -HOME_URL="https://aosc.io/" -SUPPORT_URL="https://github.com/AOSC-Dev/aosc-os-abbs" -BUG_REPORT_URL="https://github.com/AOSC-Dev/aosc-os-abbs/issues" diff --git a/testdata/simple-workspace/.ciel/container/instances/test/config.toml b/testdata/simple-workspace/.ciel/container/instances/test/config.toml deleted file mode 100644 index 6d8055a..0000000 --- a/testdata/simple-workspace/.ciel/container/instances/test/config.toml +++ /dev/null @@ -1,6 +0,0 @@ -version = 3 -extra-apt-repos = [ - "deb file:///test test testinst" -] -extra-nspawn-options = [] -use-local-repo = true diff --git a/testdata/simple-workspace/.ciel/container/instances/test/layers/.gitkeep b/testdata/simple-workspace/.ciel/container/instances/test/layers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/testdata/simple-workspace/.ciel/container/instances/tmpfs/config.toml b/testdata/simple-workspace/.ciel/container/instances/tmpfs/config.toml deleted file mode 100644 index f89c339..0000000 --- a/testdata/simple-workspace/.ciel/container/instances/tmpfs/config.toml +++ /dev/null @@ -1,7 +0,0 @@ -version = 3 -extra-apt-repos = [] -extra-nspawn-options = [] -use-local-repo = true - -[tmpfs] -size = 512 diff --git a/testdata/simple-workspace/.ciel/data/config.toml b/testdata/simple-workspace/.ciel/data/config.toml deleted file mode 100644 index 9c4a314..0000000 --- a/testdata/simple-workspace/.ciel/data/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -version = 3 -maintainer = "Bot " -dnssec = false -apt_sources = "deb https://repo.aosc.io/debs/ stable main\ndeb file:///test/ test test" -local_repo = true -local_sources = true -nspawn-extra-options = [] -branch-exclusive-output = true -volatile-mount = false -force_use_apt = false diff --git a/testdata/simple-workspace/.ciel/version b/testdata/simple-workspace/.ciel/version deleted file mode 100644 index e440e5c..0000000 --- a/testdata/simple-workspace/.ciel/version +++ /dev/null @@ -1 +0,0 @@ -3 \ No newline at end of file diff --git a/testdata/simple-workspace/TREE/.gitkeep b/testdata/simple-workspace/TREE/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/testdata/v2-workspace/.ciel/container/dist/.gitkeep b/testdata/v2-workspace/.ciel/container/dist/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/testdata/v2-workspace/.ciel/container/instances/test/layers/diff/.gitkeep b/testdata/v2-workspace/.ciel/container/instances/test/layers/diff/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/testdata/v2-workspace/.ciel/container/instances/test/layers/local/.gitkeep b/testdata/v2-workspace/.ciel/container/instances/test/layers/local/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/testdata/v2-workspace/.ciel/version b/testdata/v2-workspace/.ciel/version deleted file mode 100644 index d8263ee..0000000 --- a/testdata/v2-workspace/.ciel/version +++ /dev/null @@ -1 +0,0 @@ -2 \ No newline at end of file