diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df3b9aa..e95354e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Rust +name: Build on: [push, pull_request] @@ -16,5 +16,3 @@ jobs: - uses: actions/checkout@v2 - name: Build run: cargo build --verbose - - name: Test - run: cargo test diff --git a/.gitignore b/.gitignore index 0198098..55dfc21 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /TREE /SRCS /STATES +/.cargo .#* *.tar* diff --git a/Cargo.lock b/Cargo.lock index 1fb8193..9ebea65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,15 @@ 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" @@ -323,9 +332,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "jobserver", "libc", @@ -345,48 +354,67 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "ciel-rs" -version = "3.5.2" +name = "ciel" +version = "3.8.8" 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", - "reqwest", "serde", "sha2", - "tabwriter", "tar", "tempfile", + "test-log", + "thiserror 2.0.8", "time", "toml", - "unsquashfs-wrapper", "walkdir", - "which", "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", + "unsquashfs-wrapper", + "walkdir", + "which", + "xz2", + "zbus", +] + [[package]] name = "clap" version = "4.5.23" @@ -406,14 +434,13 @@ dependencies = [ "anstyle", "clap_lex", "strsim", - "terminal_size", ] [[package]] name = "clap_complete" -version = "4.5.38" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" +checksum = "ac2e663e3e3bed2d32d065a8404024dad306e699a04263ec59919529f803aee9" dependencies = [ "clap", ] @@ -441,15 +468,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width 0.1.14", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", ] [[package]] @@ -488,9 +515,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -507,9 +534,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -521,16 +548,6 @@ 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" @@ -590,9 +607,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" @@ -630,6 +647,33 @@ 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" @@ -916,15 +960,6 @@ 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" @@ -967,9 +1002,9 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "hyper" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -987,9 +1022,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http", @@ -1266,9 +1301,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libgit2-sys" @@ -1371,6 +1406,15 @@ 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" @@ -1394,9 +1438,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] @@ -1453,6 +1497,16 @@ 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" @@ -1534,6 +1588,12 @@ 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" @@ -1713,6 +1773,50 @@ 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" @@ -1879,9 +1983,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" dependencies = [ "core-foundation-sys", "libc", @@ -1977,6 +2081,15 @@ 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" @@ -2139,13 +2252,25 @@ dependencies = [ ] [[package]] -name = "terminal_size" -version = "0.4.1" +name = "test-log" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +checksum = "3dffced63c2b5c7be278154d76b479f9f9920ed34e7574201407f0b14e2bbb93" dependencies = [ - "rustix", - "windows-sys 0.59.0", + "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", ] [[package]] @@ -2159,11 +2284,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" dependencies = [ - "thiserror-impl 2.0.7", + "thiserror-impl 2.0.8", ] [[package]] @@ -2179,9 +2304,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" dependencies = [ "proc-macro2", "quote", @@ -2356,6 +2481,35 @@ 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]] @@ -2406,7 +2560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c233ab927e810aac155e6a22ecc44a6aaf8d66682bf7c287c80c68e2680110c9" dependencies = [ "pty-process", - "thiserror 2.0.7", + "thiserror 2.0.8", "which", ] @@ -2445,6 +2599,12 @@ 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" @@ -2571,12 +2731,12 @@ dependencies = [ [[package]] name = "which" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" +checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028" dependencies = [ "either", - "home", + "env_home", "rustix", "winsafe", ] @@ -2807,9 +2967,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.1.1" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1" dependencies = [ "async-broadcast", "async-executor", @@ -2843,9 +3003,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.1.1" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +checksum = "2c9d49ebc960ceb660f2abe40a5904da975de6986f2af0d7884b39eec6528c57" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index c4ca08d..5eb021a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,56 +1,65 @@ -[package] -name = "ciel-rs" -version = "3.5.2" +[workspace] +resolver = "2" +members = [ + ".", + "cli/", +] + +[workspace.package] +version = "3.8.8" description = "An nspawn container manager" license = "MIT" -authors = ["liushuyu "] +authors = ["liushuyu ", "xtex "] 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] -console = "0.15" -zbus = "^5" -dialoguer = { version = "0.11", features = ["fuzzy-select"] } -indicatif = "0.17" -nix = { version = "0.29", features = ["fs", "hostname", "mount", "user"] } +zbus = { version = "^5", features = ["blocking"] } +nix = { version = "0.29", features = ["fs", "hostname", "mount", "signal", "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.10" -anyhow = "1.0" +tempfile = "3.14" libsystemd-sys = "0.9" walkdir = "2" xattr = "^1" -rand = "0.8" -dotenvy = "0.15" -which = "7.0" -sha2 = "0.10" +rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } 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" - -[build-dependencies] -clap = { version = "^4", features = ["string", "env"] } -clap_complete = "^4" -anyhow = "^1" +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" [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 96625db..19cbb61 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,14 @@ ciel --help ## Installation ```bash -cargo build --release -install -Dm755 target/release/ciel-rs /usr/local/bin/ciel +cargo build --release --workspace PREFIX=/usr/local ./install-assets.sh ``` ## Dependencies Building: -- Rust w/ Cargo (Rust 1.80.0+) +- Rust w/ Cargo (Rust 1.83.0+) - C compiler - pkg-config (for detecting C library dependencies) - make (when GCC LTO is used, not needed for Clang) @@ -34,3 +33,4 @@ Runtime: Runtime Kernel: - Overlay file system +- tmpfs diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..27bb8ad --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,41 @@ +[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/build.rs b/cli/build.rs similarity index 100% rename from build.rs rename to cli/build.rs diff --git a/cli/completions/_ciel b/cli/completions/_ciel new file mode 100644 index 0000000..2c0a315 --- /dev/null +++ b/cli/completions/_ciel @@ -0,0 +1,824 @@ +#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/completions/ciel.bash b/cli/completions/ciel.bash similarity index 67% rename from completions/ciel.bash rename to cli/completions/ciel.bash index e5efb5f..4d957a7 100644 --- a/completions/ciel.bash +++ b/cli/completions/ciel.bash @@ -1,42 +1,5 @@ -_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 cmds + local i cur prev opts cmd COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -52,6 +15,9 @@ _ciel() { ciel,add) cmd="ciel__add" ;; + ciel,boot) + cmd="ciel__boot" + ;; ciel,build) cmd="ciel__build" ;; @@ -67,8 +33,8 @@ _ciel() { ciel,del) cmd="ciel__del" ;; - ciel,doctor) - cmd="ciel__doctor" + ciel,diagnose) + cmd="ciel__diagnose" ;; ciel,down) cmd="ciel__down" @@ -79,8 +45,8 @@ _ciel() { ciel,help) cmd="ciel__help" ;; - ciel,init) - cmd="ciel__init" + ciel,instconf) + cmd="ciel__instconf" ;; ciel,list) cmd="ciel__list" @@ -115,15 +81,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" ;; @@ -139,8 +105,8 @@ _ciel() { ciel__help,del) cmd="ciel__help__del" ;; - ciel__help,doctor) - cmd="ciel__help__doctor" + ciel__help,diagnose) + cmd="ciel__help__diagnose" ;; ciel__help,down) cmd="ciel__help__down" @@ -151,8 +117,8 @@ _ciel() { ciel__help,help) cmd="ciel__help__help" ;; - ciel__help,init) - cmd="ciel__help__init" + ciel__help,instconf) + cmd="ciel__help__instconf" ;; ciel__help,list) cmd="ciel__help__list" @@ -187,42 +153,21 @@ _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" ;; @@ -233,7 +178,7 @@ _ciel() { case "${cmd}" in ciel) - 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" + 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" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -251,7 +196,57 @@ _ciel() { return 0 ;; ciel__add) - opts="-h --help" + 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]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -265,26 +260,57 @@ _ciel() { return 0 ;; ciel__build) - 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 + 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in -i) - COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) + COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --resume) + --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 ;; - -c) + --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 ;; - --stage-select) + --resume) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -c) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -293,7 +319,6 @@ _ciel() { ;; esac COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - _ciel_list_packages "$cur" return 0 ;; ciel__clean) @@ -311,16 +336,12 @@ _ciel() { return 0 ;; ciel__commit) - opts="-i -h --help" + opts="-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=() ;; @@ -329,14 +350,58 @@ _ciel() { return 0 ;; ciel__config) - opts="-i -g -h --help" + 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" 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")) + --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}")) return 0 ;; *) @@ -347,19 +412,20 @@ _ciel() { return 0 ;; ciel__del) - opts="-h --help" - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - if [[ ${cur} == -* ]] ; then + opts="-a -h --all --help [INSTANCE]..." + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in *) + COMPREPLY=() ;; esac - COMPREPLY+=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__doctor) + ciel__diagnose) opts="-h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -374,16 +440,12 @@ _ciel() { return 0 ;; ciel__down) - opts="-i -h --help" + opts="-a -h --all --help [INSTANCE]..." 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=() ;; @@ -392,7 +454,7 @@ _ciel() { return 0 ;; ciel__farewell) - opts="-h --help" + opts="-f -h --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -406,7 +468,7 @@ _ciel() { return 0 ;; ciel__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" + 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" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -433,6 +495,20 @@ _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 @@ -503,7 +579,7 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__help__doctor) + ciel__help__diagnose) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -559,7 +635,7 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__help__init) + ciel__help__instconf) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -644,7 +720,7 @@ _ciel() { return 0 ;; ciel__help__repo) - opts="refresh init deinit" + opts="refresh" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -657,34 +733,6 @@ _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 @@ -769,20 +817,6 @@ _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 @@ -797,13 +831,53 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__init) - opts="-h --upgrade --help" + 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" 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=() ;; @@ -826,12 +900,24 @@ _ciel() { return 0 ;; ciel__load__os) - opts="-h --help [url]" + opts="-a -f -h --sha256 --arch --force --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=() ;; @@ -840,7 +926,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 @@ -854,16 +940,12 @@ _ciel() { return 0 ;; ciel__mount) - opts="-i -h --help" + opts="-a -h --all --help [INSTANCE]..." 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=() ;; @@ -872,13 +954,77 @@ _ciel() { return 0 ;; ciel__new) - opts="-h --from-tarball --help" + 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" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in - --from-tarball) + --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) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -890,7 +1036,7 @@ _ciel() { return 0 ;; ciel__repo) - opts="-h --help refresh init deinit help" + opts="-h --help refresh help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -903,22 +1049,8 @@ _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 init deinit help" + opts="refresh help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -931,20 +1063,6 @@ _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 @@ -959,20 +1077,6 @@ _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 @@ -987,22 +1091,8 @@ _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" + opts="-h --help [PATH]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1016,16 +1106,12 @@ _ciel() { return 0 ;; ciel__rollback) - opts="-i -h --help" + opts="-a -h --all --help [INSTANCE]..." 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=() ;; @@ -1041,7 +1127,7 @@ _ciel() { fi case "${prev}" in -i) - COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) + COMPREPLY=($(compgen -f "${cur}")) return 0 ;; *) @@ -1052,32 +1138,50 @@ _ciel() { return 0 ;; ciel__shell) - opts="-i -h --help [COMMANDS]..." + 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]..." 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")) + COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - *) - COMPREPLY=() + --local-repo) + COMPREPLY=($(compgen -W "true false" -- "${cur}")) + return 0 ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - ciel__stop) - 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")) + --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 ;; *) @@ -1087,8 +1191,8 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__update__os) - opts="-h --help" + ciel__stop) + opts="-a -h --all --help [INSTANCE]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1101,18 +1205,46 @@ _ciel() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - ciel__update__tree) - opts="-r -h --rebase --help [branch]" + 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" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in - --rebase) + --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 ;; - -r) + --remove-nspawn-opt) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -1140,4 +1272,8 @@ _ciel() { esac } -complete -F _ciel -o bashdefault -o default ciel +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 diff --git a/cli/completions/ciel.fish b/cli/completions/ciel.fish new file mode 100644 index 0000000..d5ed212 --- /dev/null +++ b/cli/completions/ciel.fish @@ -0,0 +1,225 @@ +# 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 new file mode 100644 index 0000000..ef9fc23 --- /dev/null +++ b/cli/src/actions/build.rs @@ -0,0 +1,150 @@ +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 new file mode 100644 index 0000000..5557a48 --- /dev/null +++ b/cli/src/actions/container.rs @@ -0,0 +1,121 @@ +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/src/diagnose.rs b/cli/src/actions/diagnose.rs similarity index 91% rename from src/diagnose.rs rename to cli/src/actions/diagnose.rs index 32832a4..f84c32b 100644 --- a/src/diagnose.rs +++ b/cli/src/actions/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.available_space() < (10 * 1024 * 1024 * 1024) { + let stats = statvfs(&std::fs::canonicalize(".")?)?; + if stats.blocks_available() * stats.fragment_size() < (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.available_space()))) + 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()))) } else { Ok(format!( "Disk space is sufficient ({} free of {}).", - HumanBytes(stats.available_space()), - HumanBytes(stats.total_space()) + HumanBytes(stats.blocks_available() * stats.fragment_size()), + HumanBytes(stats.blocks() * stats.fragment_size()) )) } } @@ -163,11 +163,7 @@ pub fn run_diagnose() -> Result<()> { )); continue; } - lines.push(format!( - "{} {}", - style("✓").green(), - style(msg).green().bold() - )) + lines.push(format!("{} {}", style("✓").green(), style(msg).green())) } Err(err) => { has_error = true; diff --git a/cli/src/actions/mod.rs b/cli/src/actions/mod.rs new file mode 100644 index 0000000..9b82e4c --- /dev/null +++ b/cli/src/actions/mod.rs @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..4574cdb --- /dev/null +++ b/cli/src/actions/repo.rs @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000..69961da --- /dev/null +++ b/cli/src/actions/tree.rs @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..22face8 --- /dev/null +++ b/cli/src/actions/workspace.rs @@ -0,0 +1,301 @@ +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 new file mode 100644 index 0000000..a2bd611 --- /dev/null +++ b/cli/src/cli.rs @@ -0,0 +1,484 @@ +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 new file mode 100644 index 0000000..ac3ac5e --- /dev/null +++ b/cli/src/config.rs @@ -0,0 +1,173 @@ +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/src/network.rs b/cli/src/download.rs similarity index 53% rename from src/network.rs rename to cli/src/download.rs index 6635862..b373587 100644 --- a/src/network.rs +++ b/cli/src/download.rs @@ -1,68 +1,86 @@ -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, + Arc, LazyLock, }, - thread::{self, sleep}, + thread, time::Duration, }; -const MANIFEST_URL: &str = "https://releases.aosc.io/manifest/recipe.json"; +use anyhow::{anyhow, bail, Result}; +use log::info; +use reqwest::header::CONTENT_LENGTH; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Debug, Clone)] -pub struct RootFs { - pub arch: String, - pub date: String, - pub path: String, - pub sha256sum: String, -} +use crate::make_progress_bar; -#[derive(Deserialize)] -pub struct Variant { - name: String, - squashfs: Vec, -} +const MANIFEST_URL: &str = "https://releases.aosc.io/manifest/recipe.json"; -/// AOSC OS Tarball Recipe structure -#[derive(Deserialize)] +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, - variants: Vec, + pub variants: Vec, } -static GIT_PROGRESS: LazyLock = LazyLock::new(|| { - indicatif::ProgressStyle::default_bar() - .template("[{bar:25.cyan/blue}] {pos}/{len} {msg} ({eta})") - .unwrap() -}); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Variant { + pub name: String, + pub squashfs: Vec, +} -/// Download a file from the web -pub fn download_file(url: &str) -> Result { - let client = Client::new().get(url).send()?; +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RootFsInfo { + pub arch: String, + pub date: String, + pub path: String, + pub sha256sum: String, +} - Ok(client) +pub fn http_client() -> Result { + Ok(reqwest::blocking::Client::builder() + .user_agent(concat!( + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION"), + )) + .build()?) } /// Download a file with progress indicator -pub fn download_file_progress(url: &str, file: &str) -> Result { +pub fn download_file(url: &str, file: &Path) -> Result<()> { let mut output = std::fs::File::create(file)?; - let resp = download_file(url)?; + let resp = http_client()?.get(url).send()?; + let mut total: u64 = 0; - if let Some(length) = resp.headers().get("content-length") { - total = length.to_str().unwrap_or("0").parse::().unwrap_or(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 total > 0 { // pre-allocate all the required disk space, // fails early when there is insufficient disk space available - output.allocate(total)?; + fs3::FileExt::allocate(&output, total)?; } + let progress_bar = indicatif::ProgressBar::new(total); progress_bar.set_style( indicatif::ProgressStyle::default_bar() @@ -74,33 +92,42 @@ pub fn download_file_progress(url: &str, file: &str) -> Result { std::io::copy(&mut reader, &mut output)?; progress_bar.finish_and_clear(); - Ok(total) + Ok(()) } -/// 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 +/// 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 .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() { - return Err(anyhow!("No suitable squashfs was found")); + bail!("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(); @@ -161,7 +188,7 @@ pub fn download_git(uri: &str, root: &Path) -> Result<()> { 2 => progress.set_message("Checking out files..."), _ => break, } - sleep(Duration::from_millis(100)); + std::thread::sleep(Duration::from_millis(100)); } progress.finish_and_clear(); }); @@ -175,79 +202,3 @@ 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/cli/src/logger.rs b/cli/src/logger.rs new file mode 100644 index 0000000..bd7d5f6 --- /dev/null +++ b/cli/src/logger.rs @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000..dc6d425 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,125 @@ +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 new file mode 100644 index 0000000..39155c5 --- /dev/null +++ b/cli/src/utils.rs @@ -0,0 +1,120 @@ +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 deleted file mode 100644 index f462e8a..0000000 --- a/completions/_ciel +++ /dev/null @@ -1,771 +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: ' \ -'-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/completions/ciel.fish b/completions/ciel.fish deleted file mode 100644 index 56a9a84..0000000 --- a/completions/ciel.fish +++ /dev/null @@ -1,149 +0,0 @@ -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 4c10aee..5a1e97b 100644 --- a/dbus-xml/org.freedesktop.machine1-machine.xml +++ b/dbus-xml/org.freedesktop.machine1-machine.xml @@ -1,5 +1,5 @@ +"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> @@ -9,26 +9,26 @@ - + - - + + - - + + - - + + - + @@ -64,6 +64,15 @@ + + + + + + + + + @@ -76,6 +85,10 @@ + + + + @@ -112,6 +125,16 @@ + + + + + + + + + + diff --git a/dbus-xml/org.freedesktop.machine1.xml b/dbus-xml/org.freedesktop.machine1-manager.xml similarity index 88% rename from dbus-xml/org.freedesktop.machine1.xml rename to dbus-xml/org.freedesktop.machine1-manager.xml index 80425a0..760f3e9 100644 --- a/dbus-xml/org.freedesktop.machine1.xml +++ b/dbus-xml/org.freedesktop.machine1-manager.xml @@ -1,5 +1,5 @@ +"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> @@ -9,26 +9,26 @@ - + - - + + - - + + - - + + - + @@ -120,6 +120,11 @@ + + + + + @@ -161,6 +166,18 @@ + + + + + + + + + + + + diff --git a/dbus-xml/org.freedesktop.systemd1.xml b/dbus-xml/org.freedesktop.systemd1.xml deleted file mode 100644 index c9be35e..0000000 --- a/dbus-xml/org.freedesktop.systemd1.xml +++ /dev/null @@ -1,715 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/install-assets.sh b/install-assets.sh index 55434c0..a7a070e 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 completions/_ciel "${PREFIX}/share/zsh/functions/Completion/Linux/" +install -Dvm644 cli/completions/_ciel "${PREFIX}/share/zsh/functions/Completion/Linux/" install -dv "${PREFIX}/share/fish/vendor_completions.d/" -install -Dvm644 completions/ciel.fish "${PREFIX}/share/fish/vendor_completions.d/" +install -Dvm644 cli/completions/ciel.fish "${PREFIX}/share/fish/vendor_completions.d/" install -dv "${PREFIX}/share/bash-completion/completions/" -install -Dvm644 completions/ciel.bash "${PREFIX}/share/bash-completion/completions/" +install -Dvm644 cli/completions/ciel.bash "${PREFIX}/share/bash-completion/completions/" diff --git a/src/actions/container.rs b/src/actions/container.rs deleted file mode 100644 index cfdf4d2..0000000 --- a/src/actions/container.rs +++ /dev/null @@ -1,406 +0,0 @@ -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 deleted file mode 100644 index ef52416..0000000 --- a/src/actions/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index e0c6f62..0000000 --- a/src/actions/onboarding.rs +++ /dev/null @@ -1,151 +0,0 @@ -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 deleted file mode 100644 index f05c762..0000000 --- a/src/actions/packaging.rs +++ /dev/null @@ -1,378 +0,0 @@ -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 new file mode 100644 index 0000000..6539ae9 --- /dev/null +++ b/src/build.rs @@ -0,0 +1,300 @@ +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 deleted file mode 100644 index 87ad806..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,195 +0,0 @@ -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 deleted file mode 100644 index 48eeb54..0000000 --- a/src/common.rs +++ /dev/null @@ -1,241 +0,0 @@ -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 deleted file mode 100644 index 6524f44..0000000 --- a/src/config.rs +++ /dev/null @@ -1,271 +0,0 @@ -//! 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_machine.rs b/src/dbus_machine1_machine.rs index 1607000..abf9757 100644 --- a/src/dbus_machine1_machine.rs +++ b/src/dbus_machine1_machine.rs @@ -1,28 +1,28 @@ -//! # DBus interface proxy for: `org.freedesktop.machine1.Machine` +//! # D-Bus interface proxy for: `org.freedesktop.machine1.Machine` //! -//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. +//! This code was generated by `zbus-xmlgen` `5.0.1` from D-Bus 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](https://dbus.pages.freedesktop.org/zbus/client.html) -//! section of the zbus documentation. +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. //! -//! 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: +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: //! //! * [`zbus::fdo::PeerProxy`] //! * [`zbus::fdo::IntrospectableProxy`] //! * [`zbus::fdo::PropertiesProxy`] //! -//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. - +//! 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, use zbus::proxy; - #[proxy( interface = "org.freedesktop.machine1.Machine", + assume_defaults = true, default_service = "org.freedesktop.machine1" )] pub trait Machine { @@ -38,16 +38,29 @@ 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 @@ -57,6 +70,7 @@ 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 @@ -98,6 +112,14 @@ 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; @@ -117,4 +139,8 @@ 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/src/dbus_machine1.rs b/src/dbus_machine1_manager.rs similarity index 77% rename from src/dbus_machine1.rs rename to src/dbus_machine1_manager.rs index b577b9c..19255de 100644 --- a/src/dbus_machine1.rs +++ b/src/dbus_machine1_manager.rs @@ -1,30 +1,28 @@ -//! # DBus interface proxy for: `org.freedesktop.machine1.Manager` +//! # D-Bus interface proxy for: `org.freedesktop.machine1.Manager` //! -//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. -//! Source: `org.freedesktop.machine1.xml`. +//! This code was generated by `zbus-xmlgen` `5.0.1` from D-Bus introspection data. +//! Source: `org.freedesktop.machine1-manager.xml`. //! //! You may prefer to adapt it, instead of using it verbatim. //! -//! More information can be found in the -//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) -//! section of the zbus documentation. +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. //! -//! 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: +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: //! //! * [`zbus::fdo::PeerProxy`] //! * [`zbus::fdo::IntrospectableProxy`] //! * [`zbus::fdo::PropertiesProxy`] //! -//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. - -#![allow(clippy::too_many_arguments)] -#![allow(clippy::type_complexity)] +//! 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, use zbus::proxy; - #[proxy( interface = "org.freedesktop.machine1.Manager", + assume_defaults = true, default_service = "org.freedesktop.machine1", default_path = "/org/freedesktop/machine1" )] @@ -48,10 +46,29 @@ 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, @@ -60,10 +77,11 @@ 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, @@ -73,7 +91,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 @@ -83,6 +101,7 @@ 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 @@ -92,6 +111,7 @@ pub trait Manager { ) -> zbus::Result>; /// GetImageOSRelease method + #[zbus(name = "GetImageOSRelease")] fn get_image_osrelease( &self, name: &str, @@ -104,15 +124,22 @@ 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 @@ -163,12 +190,14 @@ 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, @@ -179,6 +208,7 @@ pub trait Manager { ) -> zbus::Result<(zbus::zvariant::OwnedFd, String)>; /// RegisterMachine method + #[allow(clippy::too_many_arguments)] fn register_machine( &self, name: &str, @@ -190,6 +220,7 @@ pub trait Manager { ) -> zbus::Result; /// RegisterMachineWithNetwork method + #[allow(clippy::too_many_arguments)] fn register_machine_with_network( &self, name: &str, diff --git a/src/fs/mod.rs b/src/fs/mod.rs new file mode 100644 index 0000000..78a9869 --- /dev/null +++ b/src/fs/mod.rs @@ -0,0 +1,222 @@ +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 new file mode 100644 index 0000000..e545d96 --- /dev/null +++ b/src/fs/overlayfs.rs @@ -0,0 +1,432 @@ +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 new file mode 100644 index 0000000..afb2176 --- /dev/null +++ b/src/fs/tmpfs.rs @@ -0,0 +1,88 @@ +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 new file mode 100644 index 0000000..83c7f82 --- /dev/null +++ b/src/instance.rs @@ -0,0 +1,309 @@ +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 new file mode 100644 index 0000000..9170ae5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,193 @@ +//! 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 deleted file mode 100644 index 02a3870..0000000 --- a/src/logging.rs +++ /dev/null @@ -1,32 +0,0 @@ -#[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 7d86823..abe7cf4 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -1,429 +1,356 @@ -//! 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, - process::Command, + os::unix::ffi::OsStrExt, + path::{Path, PathBuf}, + process::{Child, Command, ExitStatus, Stdio}, + sync::Arc, + time::Duration, }; -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; -const DEFAULT_NSPAWN_OPTIONS: &[&str] = &[ - "-qb", - "--capability=CAP_IPC_LOCK", - "--system-call-filter=swapcontext", -]; +use log::{debug, info, warn}; + +use crate::{ + ContainerConfig, Error, Result, dbus_machine1_machine::MachineProxyBlocking, + dbus_machine1_manager::ManagerProxyBlocking, +}; -/// 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, +/// A systemd-nspawn machine. +pub struct Machine { + config: Arc, + rootfs_path: PathBuf, + dbus_conn: zbus::blocking::Connection, } -/// 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); +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()?, + }) } - if key_id < 0 { - return Err(anyhow!("ftok() failed.")); - } - - Ok(format!( - "{}-{:x}", - name.to_str() - .ok_or_else(|| anyhow!("Container name is not valid unicode."))?, - key_id - )) -} -/// 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))?; + /// Returns the NS name of machine. + pub fn name(&self) -> &str { + &self.config.ns_name + } - Ok(format!( - "{}-{:x}", - name.to_str() - .ok_or_else(|| anyhow!("Container name is not valid unicode."))?, - hash - )) -} + /// 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); + } -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(()); + // 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); + } } + Ok(MachineState::Starting) } - 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())); + /// 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!("Timeout waiting for container {}", ns_name)) -} + /// 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(); -/// 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)?; + let conn = zbus::blocking::Connection::system()?; + let proxy = ManagerProxyBlocking::new(&conn)?; + fs::create_dir_all(host)?; proxy.bind_mount_machine( - ns_name, - &source_path.to_string_lossy(), - mount.1, - false, + self.name(), + &fs::canonicalize(host)?.to_string_lossy(), + &guest.to_string_lossy(), + read_only, true, )?; + Ok(()) } - 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); - } - - 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()); + /// 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)) + } } - // 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()?; - - Ok(()) -} + /// 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() + ); + } -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); + machine_proxy.kill("all", nix::sys::signal::SIGKILL as i32)?; + wait_for_poweroff(&proxy, self.name())?; + machine_proxy.terminate()?; + proxy.terminate_machine(self.name())?; - if exit_code != 0 { - Err(anyhow!("Could not execute shutdown command: {}", exit_code)) - } else { Ok(()) } -} -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(()); + /// 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); } - sleep(Duration::from_secs(1)); + // 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()?) } - 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"); - } - - 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(()); + /// 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); } - // 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 + // 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())?, + }) } - // 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(()); + /// 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(()) + } + } } - - Err(anyhow!("Failed to kill the container! This may indicate a problem with your I/O, see dmesg or journalctl for more details.")) } -/// 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()?; - - 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(()) -} +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 -y --remove-config && oma clean"#; -/// 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, - }); +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(()); + } } } - // For all other errors, just return the original error object - return Err(anyhow!("{}", e)); + std::thread::sleep(Duration::from_secs_f32(((i + 1) as f32).ln().ceil())); } - 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), - }) + Err(Error::BootTimeout) } -/// 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)?, - )?); +fn wait_for_poweroff(proxy: &ManagerProxyBlocking, name: &str) -> Result<()> { + for i in 0..10 { + if proxy.get_machine(name).is_err() { + return Ok(()); } + std::thread::sleep(Duration::from_secs_f32(((i + 1) as f32).ln().ceil())); } - - Ok(instances) + Err(Error::PoweroffTimeout) } -/// 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()); - } - } - - Ok(instances) +/// 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, } -/// 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; +impl MachineState { + pub fn is_down(&self) -> bool { + matches!(self, Self::Down) + } - 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 - )?; + pub fn is_starting(&self) -> bool { + matches!(self, Self::Starting) } - formatter.flush()?; - Ok(()) + pub fn is_running(&self) -> bool { + matches!(self, Self::Running) + } } -#[test] -fn test_inspect_instance() { - println!("{:#?}", inspect_instance("alpine", "alpine")); +pub struct ExecResult { + pub status: ExitStatus, + pub stdout: String, + pub stderr: String, } -#[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() - ); +#[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(); + } + } } diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 46e13ac..0000000 --- a/src/main.rs +++ /dev/null @@ -1,402 +0,0 @@ -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/src/overlayfs.rs b/src/overlayfs.rs deleted file mode 100644 index f182df8..0000000 --- a/src/overlayfs.rs +++ /dev/null @@ -1,416 +0,0 @@ -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 a1ca4fc..a066804 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,70 +1,121 @@ -//! Local repository +use std::{ + fmt::Debug, + fs, + io::Write, + path::{Path, PathBuf}, +}; -use crate::info; -use anyhow::Result; -use console::style; +use faster_hex::hex_string; +use log::info; use sha2::{Digest, Sha256}; -use std::io::Write; -use std::{fs, io, path::Path}; use time::{format_description::FormatItem, macros::format_description, OffsetDateTime}; -mod monitor; -mod scan; +pub mod monitor; +pub mod scan; -pub use monitor::start_monitor; +use crate::Result; /// 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]"); - -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() - )) +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) + } } -/// 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 { + /// 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") + } } -/// 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(()) +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(()) + } } -/// Uninitialize the repository -pub fn deinit_repo(rootfs: &Path) -> Result<()> { - Ok(fs::remove_file( - rootfs.join("etc/apt/sources.list.d/ciel-local.list"), - )?) +#[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(), + ) + } } diff --git a/src/repo/monitor.rs b/src/repo/monitor.rs index 0131079..5c45323 100644 --- a/src/repo/monitor.rs +++ b/src/repo/monitor.rs @@ -1,31 +1,25 @@ -use crate::info; -use anyhow::Result; -use console::style; -use fs3::FileExt; use inotify::{Inotify, WatchMask}; +use log::info; use std::{ - fs::File, + fs::{self, File}, io::{Read, Seek, Write}, ops::{Deref, DerefMut}, path::Path, - sync::mpsc::Receiver, + sync::mpsc::{self, Receiver, Sender}, thread::sleep, time::Duration, }; -use super::refresh_repo; +use crate::Result; -const LOCK_FILE: &str = "debs/fresh.lock"; +use super::SimpleAptRepository; -struct FreshLockGuard { - inner: File, -} +struct FreshLockGuard(File); impl FreshLockGuard { fn new(file: File) -> Result { - file.lock_exclusive()?; - - Ok(Self { inner: file }) + fs3::FileExt::lock_exclusive(&file)?; + Ok(Self(file)) } } @@ -33,47 +27,57 @@ impl Deref for FreshLockGuard { type Target = File; fn deref(&self) -> &Self::Target { - &self.inner + &self.0 } } impl DerefMut for FreshLockGuard { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner + &mut self.0 } } impl Drop for FreshLockGuard { fn drop(&mut self) { - self.inner.unlock().ok(); + fs3::FileExt::unlock(&self.0).unwrap(); } } -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())?; +/// 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, + } } - Ok(()) + /// Stops the monitor. + pub fn stop(self) -> Result<()> { + _ = self.stop_handle.send(()); + self.thread.join().unwrap() + } } -pub fn start_monitor(pool_path: &Path, stop_token: Receiver<()>) -> Result<()> { +fn run_monitor(repo: SimpleAptRepository, stop_handle: Receiver<()>) -> Result<()> { // ensure lock exists - let lock_path = pool_path.join(LOCK_FILE); + let lock_path = repo.refresh_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()?; @@ -85,9 +89,12 @@ pub fn start_monitor(pool_path: &Path, stop_token: Receiver<()>) -> Result<()> { )?; loop { - if stop_token.try_recv().is_ok() { - return Ok(()); + match stop_handle.try_recv() { + Ok(()) => return Ok(()), + Err(mpsc::TryRecvError::Empty) => {} + Err(mpsc::TryRecvError::Disconnected) => return Ok(()), } + sleep(Duration::from_secs(1)); match inotify.read_events(&mut buffer) { Ok(_) => { @@ -95,7 +102,7 @@ pub fn start_monitor(pool_path: &Path, stop_token: Receiver<()>) -> Result<()> { ignore_next = false; continue; } - refresh_once(pool_path).ok(); + refresh_once(&repo)?; ignore_next = true; } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue, @@ -103,3 +110,27 @@ pub fn start_monitor(pool_path: &Path, stop_token: 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 71ebd9d..9a67e17 100644 --- a/src/repo/scan.rs +++ b/src/repo/scan.rs @@ -1,29 +1,100 @@ -use crate::error; -use anyhow::{anyhow, Result}; -use ar::Archive as ArArchive; -use console::style; +use core::str; 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, Write}, - path::Path, + io::{Read, Seek, SeekFrom}, + path::{Path, PathBuf}, }; -use tar::Archive as TarArchive; -use walkdir::{DirEntry, WalkDir}; +use walkdir::WalkDir; use xz2::read::XzDecoder; -enum TarFormat { +#[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 { Xzip, Gzip, Zstd, } fn collect_control(reader: R) -> Result> { - let mut tar = TarArchive::new(reader); + let mut tar = tar::Archive::new(reader); for entry in tar.entries()? { let mut entry = entry?; if entry.path_bytes().as_ref() == &b"./control"[..] { @@ -32,32 +103,11 @@ fn collect_control(reader: R) -> Result> { return Ok(buf); } } - - 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)?), - } + Err(ScanError::MissingControlFile) } -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); +fn open_deb(reader: R) -> Result> { + let mut deb = ar::Archive::new(reader); while let Some(entry) = deb.next_entry() { if entry.is_err() { continue; @@ -65,79 +115,126 @@ fn open_deb_simple(reader: R) -> Result> { let entry = entry?; let filename = entry.header().identifier(); if filename.starts_with(b"control.tar") { - let format = determine_format(filename)?; - let control = open_compressed_control(entry, &format)?; + let format = determine_compression(filename)?; + let control = open_compressed_control(entry, format)?; return Ok(control); } } - - Err(anyhow!("data archive not found or format unsupported")) + Err(ScanError::MissingControlTar) } -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(); +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)?), } - 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) +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(), + )) + } } -/// 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)?; +#[cfg(test)] +mod test { + use test_log::test; - Ok(hex_string(&hasher.finalize())) -} + use crate::{ + repo::scan::{collect_all_packages, scan_packages_simple, scan_single_deb_simple}, + test::TestDir, + }; -#[inline] -fn is_tarball(entry: &DirEntry) -> bool { - entry - .file_name() - .to_str() - .map(|s| s.ends_with(".deb")) - .unwrap_or(false) -} + #[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") + ] + ); + } -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() -} + #[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 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); - } +"## + ); } - Ok(files) + #[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 + +"## + ); + } } diff --git a/src/workspace.rs b/src/workspace.rs new file mode 100644 index 0000000..c9c29b7 --- /dev/null +++ b/src/workspace.rs @@ -0,0 +1,808 @@ +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 new file mode 100644 index 0000000..4e55f73 --- /dev/null +++ b/testdata/broken-workspace/.ciel/data/config.toml @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/testdata/broken-workspace/.ciel/version @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..4e55f73 --- /dev/null +++ b/testdata/incompat-ws-version/.ciel/data/config.toml @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/testdata/incompat-ws-version/.ciel/version @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/old-workspace/.ciel/container/instances/test/layers/diff/.gitkeep b/testdata/old-workspace/.ciel/container/instances/test/layers/diff/.gitkeep new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..4cd6827 --- /dev/null +++ b/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/acbs/forest.conf @@ -0,0 +1,2 @@ +[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 new file mode 100644 index 0000000..fe70b97 --- /dev/null +++ b/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/apt/sources.list @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..7541c4d --- /dev/null +++ b/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/autobuild/ab4cfg.sh @@ -0,0 +1,5 @@ +#!/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 new file mode 100644 index 0000000..d43d54a --- /dev/null +++ b/testdata/old-workspace/.ciel/container/instances/test/layers/local/etc/systemd/resolved.conf @@ -0,0 +1,2 @@ +[Resolve] +DNSSEC=no diff --git a/testdata/old-workspace/.ciel/data/config.toml b/testdata/old-workspace/.ciel/data/config.toml new file mode 100644 index 0000000..9c4a314 --- /dev/null +++ b/testdata/old-workspace/.ciel/data/config.toml @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/testdata/old-workspace/.ciel/version @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/testdata/simple-repo/debs/Packages b/testdata/simple-repo/debs/Packages new file mode 100644 index 0000000..be3c2d7 --- /dev/null +++ b/testdata/simple-repo/debs/Packages @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..d101709 --- /dev/null +++ b/testdata/simple-repo/debs/Release @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..613fe22 --- /dev/null +++ b/testdata/simple-workspace/.ciel/container/dist/etc/os-release @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..6d8055a --- /dev/null +++ b/testdata/simple-workspace/.ciel/container/instances/test/config.toml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..e69de29 diff --git a/testdata/simple-workspace/.ciel/container/instances/tmpfs/config.toml b/testdata/simple-workspace/.ciel/container/instances/tmpfs/config.toml new file mode 100644 index 0000000..f89c339 --- /dev/null +++ b/testdata/simple-workspace/.ciel/container/instances/tmpfs/config.toml @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..9c4a314 --- /dev/null +++ b/testdata/simple-workspace/.ciel/data/config.toml @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/testdata/simple-workspace/.ciel/version @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/testdata/simple-workspace/TREE/.gitkeep b/testdata/simple-workspace/TREE/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/testdata/v2-workspace/.ciel/container/dist/.gitkeep b/testdata/v2-workspace/.ciel/container/dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/testdata/v2-workspace/.ciel/container/instances/test/layers/diff/.gitkeep b/testdata/v2-workspace/.ciel/container/instances/test/layers/diff/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/testdata/v2-workspace/.ciel/container/instances/test/layers/local/.gitkeep b/testdata/v2-workspace/.ciel/container/instances/test/layers/local/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/testdata/v2-workspace/.ciel/version b/testdata/v2-workspace/.ciel/version new file mode 100644 index 0000000..d8263ee --- /dev/null +++ b/testdata/v2-workspace/.ciel/version @@ -0,0 +1 @@ +2 \ No newline at end of file