From 86670412856ea0e6845f2326cc68a5c257094316 Mon Sep 17 00:00:00 2001 From: Raphael Coeffic Date: Sun, 10 Nov 2024 17:07:08 +0100 Subject: [PATCH] chores: better files layout (#7) * chores: moved dive to bin directory * chores: merged image builder with root crate --- Cargo.lock | 326 +++++++++++++++--- Cargo.toml | 37 +- image_builder/Cargo.toml | 19 - src/base_image.rs | 14 +- src/bin/build-img.rs | 12 +- src/{main.rs => bin/dive.rs} | 110 ++---- .../src => src}/debug-shell/flake.lock | 0 .../src => src}/debug-shell/flake.nix | 1 + src/embedded_image.rs | 4 +- {image_builder/src => src}/etc/zshrc | 0 .../src/lib.rs => src/image_builder.rs | 273 ++++++++++----- src/lib.rs | 12 + src/pid_lookup.rs | 2 +- src/shared_mount.rs | 6 +- src/shell.rs | 127 +++++++ 15 files changed, 671 insertions(+), 272 deletions(-) delete mode 100644 image_builder/Cargo.toml rename src/{main.rs => bin/dive.rs} (66%) rename {image_builder/src => src}/debug-shell/flake.lock (100%) rename {image_builder/src => src}/debug-shell/flake.nix (98%) rename {image_builder/src => src}/etc/zshrc (100%) rename image_builder/src/lib.rs => src/image_builder.rs (69%) create mode 100644 src/lib.rs create mode 100644 src/shell.rs diff --git a/Cargo.lock b/Cargo.lock index 4c83e73..b1e32af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,6 +283,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dive" version = "0.1.2" @@ -293,16 +304,19 @@ dependencies = [ "env_logger", "exitcode", "fd-lock", - "image_builder", + "include_dir", "indicatif", "liblzma", "log", "procfs", + "regex", "rustix", "serde", "serde_json", + "sha2", "tar", "tempfile", + "ureq", "which", ] @@ -476,33 +490,143 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "image_builder" -version = "0.1.2" +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "anyhow", - "clap", - "env_logger", - "include_dir", - "indicatif", - "liblzma", - "log", - "regex", - "rustix", - "sha2", - "tar", - "tempfile", - "ureq", + "icu_normalizer", + "icu_properties", ] [[package]] @@ -625,6 +749,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "log" version = "0.4.22" @@ -819,9 +949,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.15" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "log", "once_cell", @@ -901,9 +1031,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -958,12 +1088,24 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.11.1" @@ -987,6 +1129,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tar" version = "0.4.42" @@ -1032,47 +1185,27 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "unicode-bidi" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" - [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-width" version = "0.1.14" @@ -1104,15 +1237,27 @@ dependencies = [ [[package]] name = "url" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1370,6 +1515,18 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "xattr" version = "1.3.1" @@ -1381,8 +1538,75 @@ dependencies = [ "rustix", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 1b8b01d..180062e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,40 +6,27 @@ edition = "2021" [features] embedded_image = [] -[workspace] -members = [ - "image_builder", -] - -[workspace.dependencies] +[dependencies] anyhow = "1.0.89" clap = { version = "4.5.20", features = ["derive", "env"] } +dirs = "5.0" env_logger = "0.11.5" +exitcode = "1.1.2" +fd-lock = "4.0.2" +include_dir = "0.7.4" indicatif = "0.17.8" -log = "0.4.22" liblzma = { version = "0.3.4", features = ["static"] } -rustix = { version = "0.38.37", features = ["process", "thread", "mount", "fs"] } -tar = "0.4.42" -tempfile = "3.13.0" - -[dependencies] -anyhow = { workspace = true } -clap = { workspace = true } -dirs = "5.0" -env_logger = { workspace = true } -exitcode = "1.1.2" -image_builder = { path = "image_builder" } -indicatif = { workspace = true } -log = { workspace = true } -liblzma = { workspace = true } +log = "0.4.22" procfs = { version= "0.17.0" } -rustix = { workspace = true } +regex = "1.11.1" +rustix = { version = "0.38.37", features = ["process", "thread", "mount", "fs", "runtime"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tar = { workspace = true } -tempfile = { workspace = true } +sha2 = "0.10.8" +tar = "0.4.42" +tempfile = "3.13.0" +ureq = { version = "2.10.1", default-features = false, features = ["tls", "native-certs", "gzip"] } which = "6.0.3" -fd-lock = "4.0.2" [[bin]] name = "dive" diff --git a/image_builder/Cargo.toml b/image_builder/Cargo.toml deleted file mode 100644 index fc769fe..0000000 --- a/image_builder/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "image_builder" -version = "0.1.2" -edition = "2021" - -[dependencies] -anyhow = { workspace = true } -clap = { workspace = true } -env_logger = { workspace = true } -include_dir = "0.7.4" -indicatif = { workspace = true } -liblzma = { workspace = true } -log = { workspace = true } -regex = "1.11.1" -ureq = { version = "2.10.1", default-features = false, features = ["tls", "native-certs", "gzip"] } -rustix = { workspace = true, features = ["runtime"] } -tar = { workspace = true } -tempfile = { workspace = true } -sha2 = "0.10.8" diff --git a/src/base_image.rs b/src/base_image.rs index 7b67237..51db215 100644 --- a/src/base_image.rs +++ b/src/base_image.rs @@ -1,15 +1,16 @@ use std::{fs, io, path::Path}; use anyhow::{bail, Result}; -use image_builder::progress_bar; use liblzma::read::XzDecoder; use tar::Archive; +use crate::image_builder::*; + #[cfg(feature = "embedded_image")] use crate::embedded_image; #[cfg(not(feature = "embedded_image"))] -use image_builder::BaseImageBuilder; +use crate::image_builder::BaseImageBuilder; pub fn update_base_image

(dest: &Path, image: Option

) -> Result<()> where @@ -96,7 +97,7 @@ where log::info!("removing current base image"); for dir in dest.read_dir()? { let path = dir?.path(); - if let Err(err) = image_builder::chmod(&path, |mode| mode | 0o700) { + if let Err(err) = chmod(&path, |mode| mode | 0o700) { log::error!( "could not fix permissions for {}: {}", path.display(), @@ -104,7 +105,12 @@ where ); bail!(err); } - if let Err(err) = fs::remove_dir_all(&path) { + if path.is_dir() { + if let Err(err) = fs::remove_dir_all(&path) { + log::error!("could not remove {}: {}", path.display(), err); + bail!(err); + } + } else if let Err(err) = fs::remove_file(&path) { log::error!("could not remove {}: {}", path.display(), err); bail!(err); } diff --git a/src/bin/build-img.rs b/src/bin/build-img.rs index d6445d8..76aed8a 100644 --- a/src/bin/build-img.rs +++ b/src/bin/build-img.rs @@ -5,10 +5,10 @@ use std::{ use anyhow::{bail, Result}; use clap::Parser; - -use image_builder::*; use tempfile::TempDir; +use dive::image_builder::*; + #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { @@ -28,6 +28,10 @@ struct Args { #[arg(short, long, env)] uncompressed: bool, + /// Shell + #[arg(long)] + shell_exec: bool, + /// Output name #[arg(short, long, env, default_value = "base")] output: String, @@ -104,6 +108,10 @@ fn main() -> Result<()> { base_builder.flake_dir(flake_dir); } + if args.shell_exec { + base_builder.shell_exec(true); + } + if let Some(arch) = args.arch { if !is_native_arch(&arch) && !is_qemu_supported_arch(&arch) { bail!( diff --git a/src/main.rs b/src/bin/dive.rs similarity index 66% rename from src/main.rs rename to src/bin/dive.rs index 5e319a1..2a26765 100644 --- a/src/main.rs +++ b/src/bin/dive.rs @@ -1,5 +1,4 @@ use std::{ - ffi::OsString, fs::read_link, io, os::unix::process::CommandExt, @@ -15,28 +14,17 @@ use rustix::{ runtime::{fork, Fork}, }; -mod base_image; -mod namespaces; -mod overlay; -mod pid_file; -mod pid_lookup; -mod shared_mount; - -#[cfg(feature = "embedded_image")] -mod embedded_image; - -use base_image::*; -use namespaces::*; -use overlay::*; -use pid_lookup::*; -use shared_mount::*; +use dive::base_image::*; +use dive::namespaces::*; +use dive::overlay::*; +use dive::pid_lookup::*; +use dive::shared_mount::*; +use dive::shell::*; const APP_NAME: &str = "dive"; const IMG_DIR: &str = "base-img"; const OVL_DIR: &str = "overlay"; -const DEFAULT_PATH: &str = "/usr/local/bin:/usr/bin:/bin"; - const ENV_IMG_DIR: &str = "_IMG_DIR"; const ENV_OVL_DIR: &str = "_OVL_DIR"; const ENV_LEAD_PID: &str = "_LEAD_PID"; @@ -133,76 +121,6 @@ fn prepare_shell_environment( Ok(()) } -fn exec_shell(container_id: &str) -> Result<()> { - // - // TODO: path HOME w/ user as defined by /etc/passwd - // - // TODO: find shell in this order: - // - zsh - // - bash - // - sh at last - - let proc_env = match Process::new(1).and_then(|p| p.environ()) { - Err(err) => { - bail!("could not fetch the process environment: {err}"); - } - Ok(env) => env, - }; - - let mut cmd = Command::new("zsh"); - cmd.env_clear(); - cmd.envs(&proc_env); - - let proc_path = if let Some(path) = proc_env - .get(&OsString::from("PATH")) - .filter(|v| !v.is_empty()) - { - path.to_string_lossy().into_owned() - } else { - DEFAULT_PATH.to_string() - }; - - // TODO: these variable except for TERM should be initialized in zshenv - let nix_bin_path = "/nix/.base/sbin:/nix/.base/bin:/nix/.bin"; - cmd.env("PATH", format!("{nix_bin_path}:{proc_path}")); - - if let Ok(term) = std::env::var("TERM") { - cmd.env("TERM", term); - } else { - cmd.env("TERM", "xterm"); - } - - if let Ok(lang) = std::env::var("LANG") { - cmd.env("LANG", lang); - } else { - cmd.env("LANG", "C.UTF-8"); - } - - let prompt = format!( - "%F{{cyan}}({container_id}) %F{{blue}}%~ %(?.%F{{green}}.%F{{red}})%#%f " - ); - cmd.env("PROMPT", &prompt); - - let nix_base = "/nix/.base"; - let data_dir = format!("/usr/local/share:/usr/share:{nix_base}/share"); - cmd.envs([ - ("ZDOTDIR", "/nix/etc"), - ("NIX_CONF_DIR", "/nix/etc"), - ("XDG_CACHE_HOME", "/nix/.cache"), - ("XDG_CONFIG_HOME", "/nix/.config"), - ("XDG_DATA_DIR", &data_dir), - ]); - - cmd.envs([ - ("TERMINFO_DIRS", format!("{nix_base}/share/terminfo")), - ("LIBEXEC_PATH", format!("{nix_base}/libexec")), - ("INFOPATH", format!("{nix_base}/share/info")), - ]); - - let err = cmd.exec(); - bail!("cannot exec: {}", err) -} - fn wait_for_child(child_pid: rustix::thread::Pid) -> Result<()> { // TODO: propagate return code properly log::debug!("parent pid = {}", process::id()); @@ -246,8 +164,22 @@ fn main() -> Result<()> { log::error!("{err}"); exit(1); } + + let proc_env = match Process::new(1).and_then(|p| p.environ()) { + Err(err) => { + log::error!( + "could not fetch the process environment: {err}" + ); + exit(1); + } + Ok(env) => env, + }; + + let mut shell = Shell::new(&args.container_id); + shell.env(proc_env); + // in normal cases, there is no return from exec_shell() - if let Err(err) = exec_shell(&args.container_id) { + if let Err(err) = shell.exec() { log::error!("cannot execute shell: {err}"); exit(1); } diff --git a/image_builder/src/debug-shell/flake.lock b/src/debug-shell/flake.lock similarity index 100% rename from image_builder/src/debug-shell/flake.lock rename to src/debug-shell/flake.lock diff --git a/image_builder/src/debug-shell/flake.nix b/src/debug-shell/flake.nix similarity index 98% rename from image_builder/src/debug-shell/flake.nix rename to src/debug-shell/flake.nix index 5305e62..37706bb 100644 --- a/image_builder/src/debug-shell/flake.nix +++ b/src/debug-shell/flake.nix @@ -33,6 +33,7 @@ kitty.terminfo less lsof + man nano netcat-openbsd procps diff --git a/src/embedded_image.rs b/src/embedded_image.rs index aa5b834..7bdbf96 100644 --- a/src/embedded_image.rs +++ b/src/embedded_image.rs @@ -1,10 +1,10 @@ use std::path::Path; use anyhow::Result; -use image_builder::progress_bar; use liblzma::read::XzDecoder; -use crate::install_base_image_from_reader; +use crate::base_image::install_base_image_from_reader; +use crate::image_builder::progress_bar; pub fn install_base_image

(dest: P) -> Result<()> where diff --git a/image_builder/src/etc/zshrc b/src/etc/zshrc similarity index 100% rename from image_builder/src/etc/zshrc rename to src/etc/zshrc diff --git a/image_builder/src/lib.rs b/src/image_builder.rs similarity index 69% rename from image_builder/src/lib.rs rename to src/image_builder.rs index 36cf0ec..15a4511 100644 --- a/image_builder/src/lib.rs +++ b/src/image_builder.rs @@ -13,7 +13,7 @@ use indicatif::{ProgressBar, ProgressStyle}; use liblzma::read::XzDecoder; use regex::Regex; use rustix::{ - fs::{bind_mount, recursive_bind_mount}, + fs::{bind_mount, recursive_bind_mount, unmount, UnmountFlags}, path::Arg, process::{chroot, getgid, getuid, waitpid, WaitOptions}, runtime::{fork, Fork}, @@ -23,6 +23,8 @@ use sha2::{Digest, Sha256}; use tar::Archive; use tempfile::tempdir; +use crate::shell::*; + const NIX_VERSION: &str = "2.24.9"; const NIX_CONF: &str = "experimental-features = nix-command flakes @@ -35,15 +37,16 @@ static FLAKE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/debug-shell"); // Break compilation if 'flake.nix' does not exist static _FLAKE_NIX_GUARD_: &str = include_str!("debug-shell/flake.nix"); +static BASE_SHA256: &str = "/nix/.base.sha256"; +static BASE_PATHS: &str = "/nix/.base.paths"; + static STATIC_FILES: &[(&str, &str)] = &[("/nix/etc/.zshrc", include_str!("etc/zshrc"))]; -type PostProcessFn = Box Result<()>>; - pub struct BaseImageBuilder { nix_dir: PathBuf, flake_dir: Option, - post_process: Option, + shell_exec: bool, package_output: Option, compress: bool, } @@ -60,7 +63,7 @@ impl BaseImageBuilder { BaseImageBuilder { nix_dir: nix_dir.as_ref().to_owned(), flake_dir: None, - post_process: None, + shell_exec: false, package_output: None, compress: false, } @@ -74,11 +77,8 @@ impl BaseImageBuilder { self } - pub fn post_process( - &mut self, - func: impl Fn(&Path) -> Result<()> + 'static, - ) -> &mut Self { - self.post_process.replace(Box::new(func)); + pub fn shell_exec(&mut self, shell_exec: bool) -> &mut Self { + self.shell_exec = shell_exec; self } @@ -127,19 +127,34 @@ impl BaseImageBuilder { } let hash = hasher.finalize(); + if let Err(err) = fs::write(BASE_SHA256, format!("{:x}\n", hash)) { + log::error!("failed to write hash file: {err}"); + return Self::POST_PROCESS_FAILED; + } + + let gcroots = Path::new("/nix/var/nix/gcroots"); + let gcroots_nix = gcroots.join("nix"); + let gcroots_base = gcroots.join("base"); + if let Err(err) = - fs::write("/nix/.base.sha256", format!("{:x}\n", hash)) + dump_nix_closures([&gcroots_nix, &gcroots_base], BASE_PATHS) { - log::error!("failed to write hash file: {err}"); + log::error!("failed to write store paths: {err}"); return Self::POST_PROCESS_FAILED; } - if let Err(err) = self.do_post_process(&base_path) { - log::error!("post process failed: {}", err); + if let Err(err) = dump_store_db(BASE_PATHS, "/nix/.base.reginfo") { + log::error!("failed to write base store database: {err}"); return Self::POST_PROCESS_FAILED; } - if self.do_package(&base_path).is_err() { + if self.shell_exec { + let shell = Shell::new("build-shell"); + let _ = shell.spawn(); + return Self::SUCCESS; + } + + if self.do_package().is_err() { return Self::POST_PROCESS_FAILED; } @@ -166,6 +181,10 @@ impl BaseImageBuilder { chroot(tmp)?; set_current_dir(current_dir)?; + // import initial DB + load_nix_reginfo("/nix/.reginfo")?; + + // build_builtins() match &self.flake_dir { None => { let flake_tmp = tempdir()?; @@ -193,59 +212,66 @@ impl BaseImageBuilder { } } - fn do_post_process(&self, base_path: &Path) -> Result<()> { - if let Some(func) = &self.post_process { - func(base_path) - } else { - Ok(()) - } - } - - fn do_package(&self, base_path: &Path) -> Result<()> { + fn do_package(&self) -> Result<()> { if self.package_output.is_none() { - return Ok(()); + // no packaging, run garbage collection instead + log::info!("running nix garbage collector"); + return run_nix_gc(); } let output = self.package_output.as_ref().unwrap(); + let archive_suffix = if self.compress { "tar.xz" } else { "tar" }; + let archive_name = format!("{}.{}", output.display(), archive_suffix); + if self.compress { log::info!( "packaging and compressing base image to {}", - output.display(), + archive_name, ); } else { - log::info!("packaging base image to {}", output.display()); + log::info!("packaging base image to {}", archive_name); } - let base_set = read_nix_closure(base_path)?; - let nix_set = read_nix_paths("/nix")?; + // temporary nix db mount + let tmp_db_dir = tempdir()?; + bind_mount(tmp_db_dir.path(), "/nix/var/nix/db") + .context("failed to mount temporary DB directory")?; + load_nix_reginfo("/nix/.base.reginfo")?; let mut tar_cmd = Command::new("tar"); - let archive_suffix = if self.compress { "tar.xz" } else { "tar" }; - let archive_name = format!("{}.{}", output.display(), archive_suffix); - - tar_cmd.args([ - "--directory=/nix", - "--exclude=var/nix/*", - "-c", - "-f", - &archive_name, - ]); - - if self.compress { - tar_cmd.args(["-I", "xz -T0"]); - } // prefer native tar over emulated one let current_path = std::env::var_os("PATH") .filter(|path| !path.is_empty()) .map(|path| path.to_string_lossy().into_owned() + ":") .unwrap_or_default(); + let path_env = format!("{current_path}/nix/.base/bin"); + tar_cmd.env("PATH", path_env); + + tar_cmd.args(["--directory=/nix", "-c", "-f", &archive_name]); - tar_cmd - .args([".bin", ".base", ".base.sha256", "etc", "var/nix"]) - .args(nix_set.union(&base_set).map(|p| "store/".to_owned() + p)) - .env("PATH", path_env); + if self.compress { + tar_cmd.args(["-I", "xz -T0"]); + } + + // file list + tar_cmd.args([ + ".bin", + ".base", + ".base.paths", + ".base.sha256", + ".base.reginfo", + "etc", + "var/nix/db", + "var/nix/gcroots/nix", + "var/nix/gcroots/base", + ]); + tar_cmd.args( + read_paths_set(BASE_PATHS)? + .iter() + .map(|p| p.strip_prefix("/nix/").unwrap()), + ); let tar_status = tar_cmd.status()?; @@ -257,8 +283,13 @@ impl BaseImageBuilder { } } + // remove temporary nix db mount + unmount("/nix/var/nix/db", UnmountFlags::empty()) + .context("could not unmount nix db")?; + drop(tmp_db_dir); + let sha256_name = format!("{}.sha256", output.display()); - fs::copy("/nix/.base.sha256", sha256_name) + fs::copy(BASE_SHA256, sha256_name) .context("could not copy hash file")?; Ok(()) @@ -270,45 +301,72 @@ fn nix_installer_url(version: &str, arch: &str) -> String { format!("{NIX_BASE_URL}/nix-{version}/nix-{version}-{arch}-linux.tar.xz") } -fn write_nix_paths(nix_dir: &Path) -> Result<(), io::Error> { - let mut nix_paths = String::new(); - for dir in nix_dir.join("store").read_dir()? { - nix_paths += dir?.path().file_name().unwrap().to_str().unwrap(); - nix_paths += "\n"; - } - - fs::create_dir_all(nix_dir.join(".cache"))?; - fs::write(nix_dir.join(".cache/nix_paths"), nix_paths) -} - -fn read_nix_paths

(nix_dir: P) -> Result, io::Error> +fn read_paths_set

(paths_file: P) -> Result, io::Error> where P: AsRef, { - Ok(fs::read(nix_dir.as_ref().join(".cache/nix_paths"))? - .to_string_lossy() + Ok(fs::read_to_string(paths_file.as_ref())? .lines() .map(|l| l.to_owned()) .collect()) } -fn read_nix_closure

(path: P) -> Result> +fn dump_nix_closures(paths: I, paths_file: Q) -> Result<()> where + I: IntoIterator, P: AsRef, + Q: AsRef, { - let path = path.as_ref(); - let output = Command::new("nix-store") - .args(["-qR", path.as_str()?]) + // same as "nix path-info -r [path]" + let mut cmd = Command::new("nix-store"); + cmd.arg("-qR"); + + for path in paths.into_iter() { + cmd.arg(path.as_ref().as_os_str()); + } + + let status = cmd .env("PATH", "/nix/.bin") .env("NIX_CONF_DIR", "/nix/etc") - .output()?; + .stdout(fs::File::create(paths_file.as_ref())?) + .status()?; - Ok(output - .stdout - .to_string_lossy() - .lines() - .map(|p| p.rsplit('/').next().unwrap().to_owned()) - .collect()) + if !status.success() { + if let Some(exit_code) = status.code() { + bail!("'nix-store -qR' failed with {}", exit_code); + } else { + bail!("'nix-store -qR' was interrupted by signal"); + } + } + + Ok(()) +} + +fn dump_store_db(paths_file: P, dest: Q) -> Result<()> +where + P: AsRef, + Q: AsRef, +{ + let mut cmd = Command::new("nix-store"); + + cmd.arg("--dump-db"); + cmd.args(fs::read_to_string(paths_file.as_ref())?.lines()); + + let status = cmd + .env("PATH", "/nix/.bin") + .env("NIX_CONF_DIR", "/nix/etc") + .stdout(fs::File::create(dest.as_ref())?) + .status()?; + + if !status.success() { + if let Some(exit_code) = status.code() { + bail!("'nix-store --dump-db' failed with {}", exit_code); + } else { + bail!("'nix-store --dump-db' was interrupted by signal"); + } + } + + Ok(()) } fn chmod_apply(path: &Path, func: fn(u32) -> u32) -> Result<(), io::Error> { @@ -383,12 +441,13 @@ fn download_and_install_nix( // unpack files let tar_prefix = format!("nix-{version}-{arch}-linux"); + let reginfo = Path::new(".reginfo"); for file in ar.entries()? { let mut f = file?; let fpath = f.path()?; if let Ok(fpath) = fpath.strip_prefix(&tar_prefix) { - if fpath.starts_with("store") { + if fpath.starts_with("store") || fpath == reginfo { f.unpack(dest_dir.join(fpath))?; } } @@ -399,8 +458,6 @@ fn download_and_install_nix( chmod(&path, |mode| mode & 0o555)?; } - write_nix_paths(dest)?; - Ok(()) } @@ -441,10 +498,14 @@ where } let nix_bin = dest.join(".bin"); + let gcroots = dest.join("var/nix/gcroots"); + fs::create_dir_all(&gcroots)?; + if !nix_bin.exists() { let nix_store_path = find_nix(&nix_store, NIX_VERSION)?; let nix_path = Path::new("/nix/store").join(nix_store_path); symlink(nix_path.join("bin"), nix_bin)?; + symlink(&nix_path, gcroots.join("nix"))?; } Ok(()) @@ -452,8 +513,66 @@ where fn symlink_base>(base_path: P) -> Result<(), io::Error> { let base_link = Path::new("/nix/.base"); + let gcroots = Path::new("/nix/var/nix/gcroots"); + let gcroots_base = gcroots.join("base"); + fs::create_dir_all(gcroots)?; + let _ = fs::remove_file(base_link); - symlink(&base_path, base_link) + symlink(&base_path, base_link)?; + + let _ = fs::remove_file(&gcroots_base); + symlink(&base_path, &gcroots_base) +} + +fn load_nix_reginfo>(db_dump: P) -> Result<()> { + let env = [ + ("PATH", "/nix/.bin"), + ("NIX_CONF_DIR", "/nix/etc"), + ("XDG_CACHE_HOME", "/nix/.cache"), + ("XDG_CONFIG_HOME", "/nix/.config"), + ]; + + let reginfo = fs::File::open(db_dump.as_ref())?; + let status = Command::new("nix-store") + .arg("--load-db") + .envs(env) + .stdin(reginfo) + .status()?; + + if !status.success() { + if let Some(exit_code) = status.code() { + bail!("'nix-store --load-db' failed with {}", exit_code); + } else { + bail!("'nix-store --load-db' was interrupted by signal"); + } + } + + Ok(()) +} + +fn run_nix_gc() -> Result<()> { + let env = [ + ("PATH", "/nix/.bin"), + ("NIX_CONF_DIR", "/nix/etc"), + ("XDG_CACHE_HOME", "/nix/.cache"), + ("XDG_CONFIG_HOME", "/nix/.config"), + ]; + + let status = Command::new("nix") + .args(["store", "gc"]) + .envs(env) + .stdout(Stdio::null()) + .status()?; + + if !status.success() { + if let Some(exit_code) = status.code() { + bail!("'nix store gc' failed with {}", exit_code); + } else { + bail!("'nix store gc' was interrupted by signal"); + } + } + + Ok(()) } /// Build base Nix Flake diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ba1e0ea --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +pub mod base_image; +pub mod image_builder; +pub mod namespaces; +pub mod overlay; +pub mod pid_lookup; +pub mod shared_mount; +pub mod shell; + +#[cfg(feature = "embedded_image")] +mod embedded_image; + +mod pid_file; diff --git a/src/pid_lookup.rs b/src/pid_lookup.rs index 035a9a8..bc84fa0 100644 --- a/src/pid_lookup.rs +++ b/src/pid_lookup.rs @@ -103,7 +103,7 @@ const _RUNTIMES: &[ContainerRuntime] = &[ }, ]; -pub(crate) fn pid_lookup(value: &str) -> Option { +pub fn pid_lookup(value: &str) -> Option { if let Ok(pid) = value.parse::() { return Some(pid); }; diff --git a/src/shared_mount.rs b/src/shared_mount.rs index 3656c0e..f70bab2 100644 --- a/src/shared_mount.rs +++ b/src/shared_mount.rs @@ -1,5 +1,5 @@ use std::{ - fs::{remove_file, File, OpenOptions}, + fs::{create_dir_all, remove_file, File, OpenOptions}, os::fd::{AsFd, OwnedFd}, path::{Path, PathBuf}, }; @@ -17,7 +17,8 @@ use rustix::{ }, }; -use crate::{pid_file::PidFile, OverlayBuilder, OverlayMount}; +use crate::overlay::{OverlayBuilder, OverlayMount}; +use crate::pid_file::PidFile; pub struct SharedMount { _pid_file: PidFile, @@ -117,6 +118,7 @@ impl DetachedMount { where P: AsRef, { + create_dir_all(target.as_ref())?; move_mount( self.0.as_fd(), "", diff --git a/src/shell.rs b/src/shell.rs new file mode 100644 index 0000000..2c5c92d --- /dev/null +++ b/src/shell.rs @@ -0,0 +1,127 @@ +use std::{ + collections::HashMap, + ffi::{OsStr, OsString}, + os::unix::process::CommandExt, + process::{self, exit, Command}, +}; + +use anyhow::{bail, Context, Result}; +use rustix::{ + process::{waitpid, WaitOptions}, + runtime::{fork, Fork}, +}; + +const DEFAULT_PATH: &str = "/usr/local/bin:/usr/bin:/bin"; + +pub struct Shell { + name: String, + env: HashMap, +} + +impl Shell { + pub fn new(name: &str) -> Self { + Shell { + name: name.to_owned(), + env: HashMap::new(), + } + } + + pub fn env(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef + Eq + std::hash::Hash, + V: AsRef, + { + for (ref key, ref val) in vars { + self.env + .insert(key.as_ref().to_owned(), val.as_ref().to_owned()); + } + self + } + + pub fn exec(self) -> Result<()> { + // + // TODO: path HOME w/ user as defined by /etc/passwd + // + // TODO: find shell in this order: + // - zsh + // - bash + // - sh at last + + let mut cmd = Command::new("zsh"); + cmd.env_clear(); + cmd.envs(&self.env); + + let proc_path = if let Some(path) = self + .env + .get(&OsString::from("PATH")) + .filter(|v| !v.is_empty()) + { + path.to_string_lossy().into_owned() + } else { + DEFAULT_PATH.to_string() + }; + + // TODO: these variable except for TERM should be initialized in zshenv + let nix_bin_path = "/nix/.base/sbin:/nix/.base/bin:/nix/.bin"; + cmd.env("PATH", format!("{nix_bin_path}:{proc_path}")); + + if let Ok(term) = std::env::var("TERM") { + cmd.env("TERM", term); + } else { + cmd.env("TERM", "xterm"); + } + + if let Ok(lang) = std::env::var("LANG") { + cmd.env("LANG", lang); + } else { + cmd.env("LANG", "C.UTF-8"); + } + + let prompt = format!( + "%F{{cyan}}({}) %F{{blue}}%~ %(?.%F{{green}}.%F{{red}})%#%f ", + self.name, + ); + cmd.env("PROMPT", &prompt); + + let nix_base = "/nix/.base"; + let data_dir = format!("/usr/local/share:/usr/share:{nix_base}/share"); + cmd.envs([ + ("ZDOTDIR", "/nix/etc"), + ("NIX_CONF_DIR", "/nix/etc"), + ("XDG_CACHE_HOME", "/nix/.cache"), + ("XDG_CONFIG_HOME", "/nix/.config"), + ("XDG_DATA_DIR", &data_dir), + ]); + + cmd.envs([ + ("TERMINFO_DIRS", format!("{nix_base}/share/terminfo")), + ("LIBEXEC_PATH", format!("{nix_base}/libexec")), + ("INFOPATH", format!("{nix_base}/share/info")), + ]); + + let err = cmd.exec(); + bail!("cannot exec: {}", err) + } + + pub fn spawn(self) -> Result<()> { + match unsafe { fork()? } { + Fork::Child(_) => { + if let Err(err) = self.exec() { + log::error!("cannot execute shell: {err}"); + exit(1); + } + exit(0); + } + Fork::Parent(child_pid) => wait_for_child(child_pid), + } + } +} + +pub fn wait_for_child(child_pid: rustix::thread::Pid) -> Result<()> { + // TODO: propagate return code properly + log::debug!("parent pid = {}", process::id()); + let _ = waitpid(Some(child_pid), WaitOptions::empty()) + .context("waitpid failed")?; + Ok(()) +}