diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..d565ad84 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", + "jnoortheen.nix-ide", + "mkhl.direnv" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..cba9ad39 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnSave": true, + "files.autoSave": "onFocusChange", +} diff --git a/codchi/cli/Cargo.toml b/codchi/cli/Cargo.toml index ff491117..70bf0f94 100644 --- a/codchi/cli/Cargo.toml +++ b/codchi/cli/Cargo.toml @@ -66,3 +66,4 @@ once_cell = "1.19.0" url = "2.5.0" git-url-parse = "0.4.4" lazy-regex = "3.1.0" +log = "0.4" diff --git a/codchi/cli/src/cli.rs b/codchi/cli/src/cli.rs index bd57803d..ea72fe31 100644 --- a/codchi/cli/src/cli.rs +++ b/codchi/cli/src/cli.rs @@ -1,17 +1,27 @@ -use std::{fmt::Display, str::FromStr}; +pub use self::module::*; use clap::builder::*; use clap::*; -use clap_verbosity_flag::{Verbosity, WarnLevel}; +use clap_verbosity_flag::{InfoLevel, LogLevel, Verbosity}; use git_url_parse::GitUrl; use lazy_regex::regex_captures; - -// pub use self::ctrl::*; -pub use self::module::*; - -// pub static CLI_ARGS: OnceCell = OnceCell::new(); - -type DefaultLogLevel = WarnLevel; +use log::Level; +use once_cell::sync::Lazy; +use std::{fmt::Display, str::FromStr, sync::OnceLock}; + +#[allow(dead_code)] +pub static CLI_ARGS: OnceLock = OnceLock::new(); +#[allow(dead_code)] +pub static DEBUG: Lazy = Lazy::new(|| { + CLI_ARGS + .get() + .and_then(|cli| cli.verbose.log_level()) + .or(::default()) + .unwrap() + > Level::Debug +}); + +type DefaultLogLevel = InfoLevel; /// codchi #[derive(Debug, Parser, Clone)] @@ -21,7 +31,7 @@ type DefaultLogLevel = WarnLevel; long_version = format!("v{}\n{}", option_env!("CARGO_PKG_VERSION").unwrap_or(""), option_env!("CODCHI_GIT_COMMIT").unwrap_or(""), - ) + ), )] pub struct Cli { #[command(flatten)] @@ -40,7 +50,6 @@ pub enum Cmd { // #[command(subcommand)] // #[clap(aliases = &["ctrl"])] // Controller(ControllerCmd), - Status {}, /// Create a new code machine @@ -53,6 +62,17 @@ pub enum Cmd { options: AddModuleOptions, }, + /// Execute (interactive) command inside machine. + #[clap(aliases = &["run"])] + Exec { + /// Name of the code machine + name: String, + + /// Command with arguments to run + #[arg(trailing_var_arg = true)] + cmd: Vec, + }, + /// Apply changes to a code machine Rebuild { /// Name of the code machine @@ -65,6 +85,16 @@ pub enum Cmd { name: String, }, + /// Delete code machine with all associated files + Delete { + /// Don't prompt for confirmation and delete immediately + #[arg(long)] + im_really_sure: bool, + + /// Name of the code machine + name: String, + }, + /// Manage modules of code machines #[command(subcommand)] #[clap(aliases = &["mod"])] @@ -147,12 +177,11 @@ mod module { /// Id of the module (You can list them with `codchi module ls NAME`) id: usize, }, - - /// Fetch module updates - Update { - /// Name of the code machine - name: String, - }, + // /// Fetch module updates + // Update { + // /// Name of the code machine + // name: String, + // }, } #[derive(clap::Args, Debug, Clone)] diff --git a/codchi/cli/src/config.rs b/codchi/cli/src/config.rs index 1e111274..3d751403 100644 --- a/codchi/cli/src/config.rs +++ b/codchi/cli/src/config.rs @@ -1,5 +1,6 @@ use crate::{ - consts::{self, host, ToPath, NIX_SYSTEM}, platform, util::UtilExt + consts::{host, ToPath}, + util::UtilExt, }; use anyhow::{Context, Result}; use fs4::FileExt; @@ -56,6 +57,8 @@ impl MutableConfig { } pub fn write(mut self) -> Result<()> { + self.get_machines().set_implicit(false); + let res = self.doc.to_string(); let bytes = res.as_bytes(); self.file.set_len(bytes.len() as u64)?; @@ -68,11 +71,9 @@ impl MutableConfig { if !self.doc.contains_table("machines") { self.doc["machines"] = table(); } - let table = self.doc["machines"] + self.doc["machines"] .as_table_mut() - .expect("Config toml doesn't contain key 'machines'"); - table.set_implicit(true); - table + .expect("Config toml doesn't contain key 'machines'") } pub fn get_machine(&mut self, name: &str) -> Option<&mut toml_edit::Table> { @@ -135,57 +136,6 @@ pub struct MachineConfig { pub modules: Vec, } -impl MachineConfig { - pub fn read_config(name: &str) -> Result> { - Ok(Config::read()?.machines.get(name).cloned()) - } - - pub fn gen_flake(&self) -> String { - let codchi_url = consts::CODCHI_FLAKE_URL; - let module_inputs = self - .modules - .iter() - .enumerate() - .map(|(idx, url)| format!(r#" "{idx}".url = "{}";"#, url.to_nix_url())) - .join("\n"); - let driver = platform::NIXOS_DRIVER_NAME; - let nixpkgs = if let Some(idx) = self.nixpkgs_from { - format!(r#"inputs."{idx}".inputs.nixpkgs"#) - } else { - "inputs.codchi.inputs.nixpkgs".to_string() - }; - let modules = self - .modules - .iter() - .enumerate() - .map(|(idx, url)| { - format!( - r#" {{ module = inputs."{idx}".{module_name}; }}"#, - module_name = url.flake_attr.0 - ) - }) - .join("\n"); - format!( - r#"{{ - inputs = {{ - codchi.url = "{codchi_url}"; -{module_inputs} - }}; - outputs = inputs: {{ - nixosConfigurations.default = inputs.codchi.lib.codeMachine {{ - driver = "{driver}"; - system = "{NIX_SYSTEM}"; - nixpkgs = {nixpkgs}; - modules = [ -{modules} - ]; - }}; - }}; -}}"# - ) - } -} - pub type CodchiModule = FlakeUrl; pub mod flake_attr { diff --git a/codchi/cli/src/consts.rs b/codchi/cli/src/consts.rs index ee139076..93843efb 100644 --- a/codchi/cli/src/consts.rs +++ b/codchi/cli/src/consts.rs @@ -97,3 +97,14 @@ pub mod machine { format!("codchi-{name}") } } + +pub mod user { + pub const ROOT_UID: &str = "0"; + pub const ROOT_GID: &str = "0"; + pub const ROOT_HOME: &str = "/root"; + + pub const DEFAULT_NAME: &str = "codchi"; + pub const DEFAULT_HOME: &str = "/home/codchi"; + pub const DEFAULT_UID: &str = "1000"; + pub const DEFAULT_GID: &str = "100"; +} diff --git a/codchi/cli/src/main.rs b/codchi/cli/src/main.rs index 56d796dd..624758cf 100644 --- a/codchi/cli/src/main.rs +++ b/codchi/cli/src/main.rs @@ -2,8 +2,8 @@ #![deny(unused_crate_dependencies)] use crate::{ - cli::{Cli, Cmd}, - platform::{ConfigStatus, Driver, Machine, MachineDriver, PlatformStatus}, + cli::{Cli, Cmd, CLI_ARGS}, + platform::{ConfigStatus, Driver, Machine, PlatformStatus}, }; use base64::{prelude::BASE64_STANDARD, Engine}; use clap::*; @@ -30,22 +30,26 @@ fn main() -> anyhow::Result<()> { trace!("Started codchi with args: {:?}", cli); - // CLI_ARGS - // .set(cli.clone()) - // .expect("Only main is allowed to set CLI_ARGS."); + CLI_ARGS + .set(cli.clone()) + .expect("Only main is allowed to set CLI_ARGS."); let _ = Driver::store(); match &cli.command.unwrap_or(Cmd::Status {}) { Cmd::Status {} => print_status(Machine::list()?), - Cmd::Init { empty, options } => module::init(*empty, &options)?, - Cmd::Rebuild { name } => Machine::build(name)?, - Cmd::Update { name } => Machine::update(name)?, + Cmd::Init { empty, options } => alert_dirty(module::init(*empty, &options)?), + Cmd::Rebuild { name } => Machine::by_name(name)?.build()?, + Cmd::Update { name } => alert_dirty(Machine::by_name(name)?.update()?), + Cmd::Exec { name, cmd } => Machine::by_name(name)?.exec(cmd)?, + Cmd::Delete { + name, + im_really_sure, + } => Machine::by_name(name)?.delete(*im_really_sure)?, Cmd::Module(cmd) => match cmd { cli::ModuleCmd::List { name } => module::list(name)?, - cli::ModuleCmd::Add(opts) => module::add(opts)?, - cli::ModuleCmd::Delete { name, id } => module::delete(name, *id)?, - cli::ModuleCmd::Update { name: _ } => todo!(), + cli::ModuleCmd::Add(opts) => alert_dirty(module::add(opts)?), + cli::ModuleCmd::Delete { name, id } => alert_dirty(module::delete(name, *id)?), }, } @@ -54,6 +58,32 @@ fn main() -> anyhow::Result<()> { Ok(()) } +fn alert_dirty(machine: Machine) { + match machine.config_status { + ConfigStatus::NotInstalled => { + info!( + "{} is not installed yet. Install with `codchi rebuild {}`", + machine.name, machine.name + ); + } + ConfigStatus::Modified => { + info!( + "{} was modified. Apply changes with `codchi rebuild {}`", + machine.name, machine.name + ); + } + ConfigStatus::UpdatesAvailable => { + info!( + "{} has been updated upstream. Update with `codchi rebuild {}`", + machine.name, machine.name + ); + } + ConfigStatus::UpToDate => { + info!("Everything up to date!"); + } + } +} + fn print_status(machines: Vec) { use comfy_table::*; let mut table = Table::new(); @@ -67,7 +97,7 @@ fn print_status(machines: Vec) { table.add_row(vec![ Cell::new(&machine.name), match machine.config_status { - ConfigStatus::NotInstalled => Cell::new("Never built").fg(Color::DarkYellow), + ConfigStatus::NotInstalled => Cell::new("Not installed yet").fg(Color::Red), ConfigStatus::Modified => Cell::new("Modified").fg(Color::Yellow), ConfigStatus::UpdatesAvailable => Cell::new("Updates available").fg(Color::Yellow), ConfigStatus::UpToDate => Cell::new("Up to date").fg(Color::Green), diff --git a/codchi/cli/src/module.rs b/codchi/cli/src/module.rs index 8c2580e0..016e0145 100644 --- a/codchi/cli/src/module.rs +++ b/codchi/cli/src/module.rs @@ -1,7 +1,7 @@ use crate::{ cli::AddModuleOptions, config::{ - flake_attr, Config, CodchiModule, FlakeScheme, FlakeUrl, MachineConfig, MutableConfig, + flake_attr, CodchiModule, Config, FlakeScheme, FlakeUrl, MachineConfig, MutableConfig, }, platform::{nix::NixDriver, *}, util::UtilExt, @@ -12,7 +12,7 @@ use spinoff::{spinners, Color, Spinner}; use std::fmt::Display; use toml_edit::{ser::to_document, Key}; -pub fn init(empty: bool, opts: &AddModuleOptions) -> Result<()> { +pub fn init(empty: bool, opts: &AddModuleOptions) -> Result { let mut cfg = MutableConfig::open()?; let machines = cfg.get_machines(); @@ -56,19 +56,23 @@ which might decrease reproducibility but is faster.", } }; - println!("{}", machine.gen_flake()); - machines.insert_formatted( &Key::new(&opts.name), to_document(&machine)?.as_item().clone(), ); cfg.write()?; - Machine::write(&opts.name, &machine)?; - Ok(()) + let machine = Machine { + name: opts.name.to_string(), + config: machine, + config_status: ConfigStatus::NotInstalled, + platform_status: PlatformStatus::NotInstalled, + }; + machine.write_flake()?; + Ok(machine) } -pub fn add(opts: &AddModuleOptions) -> std::result::Result<(), anyhow::Error> { +pub fn add(opts: &AddModuleOptions) -> Result { let mut cfg = MutableConfig::open()?; let machine = cfg .get_machine(&opts.name) @@ -100,12 +104,9 @@ pub fn add(opts: &AddModuleOptions) -> std::result::Result<(), anyhow::Error> { modules.push(mod_str); cfg.write()?; - Machine::write( - &opts.name, - &MachineConfig::read_config(&opts.name)?.expect("Failed to read just created machine."), - )?; - - Ok(()) + let machine = Machine::by_name(&opts.name)?; + machine.write_flake()?; + machine.update_status() } /// List modules of a code machine @@ -140,7 +141,7 @@ pub fn list(name: &String) -> Result<()> { Ok(()) } -pub fn delete(name: &str, id: usize) -> std::result::Result<(), anyhow::Error> { +pub fn delete(name: &str, id: usize) -> Result { let mut cfg = MutableConfig::open()?; let machine = cfg .get_machine(name) @@ -161,9 +162,9 @@ pub fn delete(name: &str, id: usize) -> std::result::Result<(), anyhow::Error> { cfg.write()?; - // TODO properly delete machine (Driver) - - Ok(()) + let machine = Machine::by_name(&name)?; + machine.write_flake()?; + machine.update_status() } type HasNixpkgs = bool; diff --git a/codchi/cli/src/platform/cmd.rs b/codchi/cli/src/platform/cmd.rs index cf751e46..19761cec 100644 --- a/codchi/cli/src/platform/cmd.rs +++ b/codchi/cli/src/platform/cmd.rs @@ -1,4 +1,8 @@ -use std::{io, process::ExitStatus, str::FromStr}; +use std::{ + io, + process::{exit, ExitStatus}, + str::FromStr, +}; use thiserror::Error; use crate::util::UtilExt; @@ -49,25 +53,22 @@ fn to_result(cmd: Command, output: std::process::Output) -> Result> { #[derive(Debug, Clone)] pub struct Command { pub program: Program, - pub uid: Option, + pub user: Option, pub cwd: Option, - pub input: Option, pub output: Output, } #[derive(Debug, Clone)] pub enum Program { - Single { program: String, args: Vec }, + // Raw { program: String, args: Vec }, + Run { program: String, args: Vec }, Script(String), } -impl Program { - fn needs_stdin(&self) -> bool { - match self { - Program::Script(_) => true, - _ => false, - } - } +#[derive(Debug, Clone)] +pub enum LinuxUser { + Root, + Default, } #[derive(Debug, Clone)] @@ -80,13 +81,12 @@ pub enum Output { impl Command { pub fn new(program: &str, args: &[&str]) -> Self { Self { - program: Program::Single { + program: Program::Run { program: program.to_string(), args: args.iter().map(|arg| arg.to_string()).collect(), }, - uid: None, + user: None, cwd: None, - input: None, output: Output::Collect, } } @@ -94,17 +94,16 @@ impl Command { pub fn script(script: String) -> Self { Self { program: Program::Script(script), - uid: None, + user: None, cwd: None, - input: None, output: Output::Collect, } } - // pub fn uid(mut self, uid: usize) -> Self { - // self.uid = Some(uid); - // self - // } + pub fn user(mut self, user: LinuxUser) -> Self { + self.user = Some(user); + self + } pub fn cwd>(mut self, cwd: P) -> Self { self.cwd = Some( @@ -124,14 +123,21 @@ impl Command { } pub trait CommandDriver { - fn build(&self, uid: Option, cwd: Option) -> std::process::Command; + fn build(&self, uid: Option, cwd: Option) -> std::process::Command; fn spawn(&self, spec: Command) -> Result { - let mut cmd = self.build(spec.uid, spec.cwd); + let mut cmd = self.build(spec.user, spec.cwd); // let mut cmd = std::process::Command::new("cat"); let stdin = match spec.program { - Program::Single { program, args } => { + // Program::Raw { program, args } => { + // cmd.arg(program); + // for arg in args.iter() { + // cmd.arg(arg); + // } + // None + // } + Program::Run { program, args } => { cmd.args(&["run", &program]); for arg in args.iter() { cmd.arg(arg); @@ -184,6 +190,10 @@ pub trait CommandDriver { Ok(()) } + fn exec(&self, spec: Command) -> Result<()> { + exit(self.spawn(spec)?.wait()?.code().unwrap_or(1)) + } + fn output_json(&self, spec: Command) -> Result where T: for<'de> Deserialize<'de>, diff --git a/codchi/cli/src/platform/linux/lxd.rs b/codchi/cli/src/platform/linux/lxd.rs new file mode 100644 index 00000000..e996fbe7 --- /dev/null +++ b/codchi/cli/src/platform/linux/lxd.rs @@ -0,0 +1,255 @@ +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::process::Output; +use std::{io, process::Command}; + +pub fn lxc(args: &[&str]) -> io::Result<()> { + let mut cmd = Command::new("lxc"); + cmd.arg("-q"); + for arg in args.iter() { + cmd.arg(arg); + } + + log::trace!("Running LXC command: {:?}", &cmd); + + let status = cmd.spawn()?.wait()?; + if status.success() { + Ok(()) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!("LXD {:?} failed with {}", args, status), + )) + } +} + +pub fn lxc_output(args: &[&str]) -> io::Result { + let mut cmd = Command::new("lxc"); + cmd.arg("-q"); + // see https://github.com/rust-lang/rust/issues/30098#issuecomment-160346319 + // cmd.stdin(Stdio::inherit()); + // cmd.stdout(Stdio::inherit()); + // cmd.stderr(Stdio::inherit()); + for arg in args.iter() { + cmd.arg(arg); + } + + log::trace!("Running LXC command: {:?}", &cmd); + + cmd.output() +} +pub fn lxc_output_ok(args: &[&str]) -> io::Result> { + let output = lxc_output(&args)?; + if output.status.success() { + Ok(output.stdout) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!("LXD {:?} failed with {}", args, output.status), + )) + } +} + +pub mod image { + use super::*; + + pub fn import>(path: P, alias: &str) -> io::Result<()> { + lxc(&[ + "image", + "import", + &format!("{}", path.as_ref().display()), + "--alias", + alias, + ]) + } + + pub fn delete(name: &str) -> io::Result<()> { + lxc(&["image", "delete", name]) + } + + pub fn init(image_name: &str, container_name: &str) -> io::Result<()> { + lxc(&["init", image_name, container_name]) + } +} + +pub mod container { + use std::path::PathBuf; + + use anyhow::Context; + use itertools::Itertools; + use log::warn; + + use crate::{ + consts::{user, ToPath}, + platform::PlatformStatus, + }; + + use super::*; + + #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] + /// LXD container information + pub struct Info { + pub status: String, + } + + pub fn start(name: &str) -> io::Result<()> { + lxc(&["start", name]) + } + + pub fn stop(name: &str, force: bool) -> io::Result<()> { + if force { + lxc(&["stop", "--force", name]) + } else { + lxc(&["stop", name]) + } + } + + pub fn list(filter: &str) -> io::Result> { + let json = lxc_output_ok(&["list", filter, "--format", "json"])?; + serde_json::from_slice::>(&json).map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!("LXD info: failed to parse json: {}", err), + ) + }) + } + + pub fn get_info(name: &str) -> io::Result> { + list(name).map(|mut list| { + if list.len() == 1 { + Some(list.remove(0)) + } else { + None + } + }) + } + + pub fn get_platform_status(name: &str) -> io::Result { + Ok(match container::get_info(name)? { + None => PlatformStatus::NotInstalled, + Some(container) => { + if container.status == "Running" { + PlatformStatus::Running + } else { + PlatformStatus::Stopped + } + } + }) + } + + pub fn delete(name: &str, force: bool) -> io::Result<()> { + if force { + lxc(&["delete", name, "--force"]) + } else { + lxc(&["delete", name]) + } + } + pub fn config_set(name: &str, cfg: &str) -> io::Result<()> { + lxc(&["config", "set", name, cfg]) + } + + pub fn config_mount>( + container_name: &str, + disk_name: &str, + source: P, + dest: &str, + ) -> io::Result<()> { + lxc(&[ + "config", + "device", + "add", + container_name, + disk_name, + "disk", + &format!("source={}", source.as_ref().display()), + &format!("path={}", dest), + ]) + } + + #[allow(dead_code)] + pub fn get_logs(name: &str) -> io::Result { + let out = lxc_output_ok(&["console", name, "--show-log"])?; + Ok(String::from_utf8_lossy(&out).lines().dropping(2).join("\n")) + } + + pub fn install<'a, P, I>(name: &str, rootfs: P, mounts: I) -> anyhow::Result<()> + where + P: AsRef, + I: IntoIterator, + { + (|| { + image::import(&rootfs, &name).with_context(|| { + format!( + "Failed to import LXD image {name} from {}.", + rootfs.as_ref().to_string_lossy() + ) + })?; + + image::init(&name, &name).with_context(|| { + format!( + "Failed to create LXD container {name} from {}.", + rootfs.as_ref().to_string_lossy() + ) + })?; + + image::delete(&name)?; + + container::config_set(&name, "security.nesting=true")?; + + // Map current host user to root in containers to allow accessing their files. + // Although this prevents access to other users' files (like /home in a code + // machine) from the host, root inside the container should be able to access + // them. TODO: check /etc/sub{u,g}id for correctness + let idmap = { + #[link(name = "c")] + extern "C" { + /// Get current uid via libc + fn geteuid() -> u32; + /// Get current gid via libc + fn getegid() -> u32; + } + unsafe { + format!( + "uid {hostuid} {guestuid}\ngid {hostgid} {guestgid}", + hostuid = geteuid(), + guestuid = user::ROOT_UID, + hostgid = getegid(), + guestgid = user::ROOT_GID, + ) + } + }; + container::config_set(&name, &format!("raw.idmap={idmap}"))?; + + for (source, dest) in mounts { + let source = source.get_or_create()?; + container::config_mount( + &name, + dest.strip_prefix("/").unwrap_or(&dest), + &source, + &dest, + ) + .with_context(|| { + format!( + "Failed to mount LXD device '{}' at path {dest} to container {name}.", + source.to_string_lossy(), + ) + })?; + } + + container::start(&name).with_context(|| { + format!( + "Failed to start LXD container {name} from {}.", + rootfs.as_ref().to_string_lossy() + ) + })?; + + Ok(()) + })() + .map_err(|err| { + warn!("Removing leftovers of LXD container {name}..."); + let _ = image::delete(&name); + let _ = container::delete(&name, true); + err + }) + } +} diff --git a/codchi/cli/src/platform/linux/mod.rs b/codchi/cli/src/platform/linux/mod.rs index cf1da525..2e4d4b67 100644 --- a/codchi/cli/src/platform/linux/mod.rs +++ b/codchi/cli/src/platform/linux/mod.rs @@ -1,12 +1,15 @@ -use super::{private::Private, CommandDriver, NixDriver, Store}; +use super::{private::Private, CommandDriver, LinuxUser, NixDriver, Store}; use crate::{ - consts::{self, *}, + cli::DEBUG, + consts::{self, machine::machine_name, *}, platform::{Machine, MachineDriver, PlatformStatus}, }; use anyhow::{Context, Result}; use log::*; use std::{env, path::PathBuf}; +pub mod lxd; + pub const NIX_STORE_PACKAGE: &str = "store-lxd"; pub const NIXOS_DRIVER_NAME: &str = "lxd"; @@ -15,104 +18,39 @@ pub struct StoreImpl {} impl Store for StoreImpl { fn start_or_init_container(_: Private) -> Result { // TODO add link to docs - let info = lxd::container::get_info(consts::CONTAINER_STORE_NAME).context( + let status = lxd::container::get_platform_status(consts::CONTAINER_STORE_NAME).context( "Failed to run LXD. It seems like LXD is not installed or set up correctly! Please see the codchi docs for the setup instructions!", )?; + trace!("LXD store container status: {status:#?}"); - match info { - Some(info) if info.status == "Running" => { - trace!("LXD controller container is already running."); + match status { + PlatformStatus::NotInstalled => { + let rootfs = env::var("CODCHI_LXD_CONTAINER_STORE") + .map(|dir| PathBuf::from(dir)) + .context("Failed reading $CODCHI_LXD_CONTAINER_STORE from environment. This indicates a broken build.")?; + let mounts = vec![ + ( + host::DIR_CONFIG.clone(), + store::DIR_CONFIG.to_str().unwrap(), + ), + (host::DIR_DATA.clone(), store::DIR_DATA.to_str().unwrap()), + (host::DIR_NIX.clone(), store::DIR_NIX.to_str().unwrap()), + ]; + lxd::container::install(consts::CONTAINER_STORE_NAME, &rootfs, mounts)?; Ok(StoreImpl {}) } - Some(info) => { - trace!("Got info of LXD controller container: {info:#?}"); - + PlatformStatus::Stopped => { lxd::container::start(consts::CONTAINER_STORE_NAME) .context("Failed to start store container")?; Ok(StoreImpl {}) } - None => { - (|| { - let rootfs = env::var("CODCHI_LXD_CTRL_ROOTFS") - .map(|dir| PathBuf::from(dir)) - .context("Failed reading $CODCHI_LXD_CTRL_ROOTFS from environment. This indicates a broken build.")?; - - lxd::image::import(rootfs, consts::CONTAINER_STORE_NAME) - .context("Failed to import LXD controller image.")?; - - lxd::image::init(consts::CONTAINER_STORE_NAME, consts::CONTAINER_STORE_NAME) - .context("Failed to create LXD controller container.")?; - - lxd::image::delete(consts::CONTAINER_STORE_NAME)?; - - lxd::container::config_set( - consts::CONTAINER_STORE_NAME, - "security.nesting=true", - )?; - // Map current host user to root in containers to allow accessing their files. - // Although this prevents access to other users' files (like /home in a code - // machine) from the host, root inside the container should be able to access - // them. TODO: check /etc/sub{u,g}id for correctness - let idmap = { - #[link(name = "c")] - extern "C" { - /// Get current uid via libc - fn geteuid() -> u32; - /// Get current gid via libc - fn getegid() -> u32; - } - unsafe { format!("uid {} 0\ngid {} 0", geteuid(), getegid()) } - }; - lxd::container::config_set( - consts::CONTAINER_STORE_NAME, - &format!("raw.idmap={idmap}"), - )?; - - fn create_and_mount(source: &PathBuf, dest: &PathBuf) -> Result<()> { - let source = source.get_or_create()?; - let dest = dest.to_str().expect("Invalid UTF8 in store path."); - lxd::container::config_mount( - consts::CONTAINER_STORE_NAME, - dest.strip_prefix("/").unwrap_or(&dest), - &source, - &dest, - ) - .with_context(|| { - format!( - "Failed to mount LXD device '{}' at path {} to controller", - source.to_string_lossy(), - dest - ) - }) - } - create_and_mount(&host::DIR_CONFIG, &store::DIR_CONFIG)?; - create_and_mount(&host::DIR_DATA, &store::DIR_DATA)?; - create_and_mount(&host::DIR_NIX, &store::DIR_NIX)?; - - lxd::container::start(consts::CONTAINER_STORE_NAME) - .context("Failed to start the store container")?; - - Ok(StoreImpl {}) - })().map_err(|err| { - warn!("Removing leftovers of LXD store container..."); - let _ = lxd::image::delete(consts::CONTAINER_STORE_NAME); - let _ = lxd::container::delete(consts::CONTAINER_STORE_NAME, true); - err - }) - } + PlatformStatus::Running => Ok(StoreImpl {}), } } - //fn init_controller(&self) -> Result<()> { - - //fn stop_controller(&self) -> Result<()> { - // lxd::container::stop(consts::CONTROLLER_NAME, true)?; - // Ok(()) - //} - fn cmd(&self) -> impl CommandDriver + NixDriver { LxdCommandDriver { name: consts::CONTAINER_STORE_NAME.to_string(), @@ -120,202 +58,98 @@ impl Store for StoreImpl { } } -pub struct LxdCommandDriver { - pub name: String, -} -impl CommandDriver for LxdCommandDriver { - fn build(&self, uid: Option, cwd: Option) -> std::process::Command { - let mut cmd = std::process::Command::new("lxc"); - cmd.arg("-q"); - cmd.args(&["exec", &self.name]); - if let Some(cwd) = &cwd { - cmd.args(&["--cwd", &cwd]); - } - if let Some(uid) = &uid { - cmd.args(&["--user", &uid.to_string()]); - } - // if needs_stdin { - // cmd.arg("-T"); - // } - cmd.arg("--"); - cmd - } -} -impl NixDriver for LxdCommandDriver {} - impl MachineDriver for Machine { - fn read_platform(name: &str, _: Private) -> Result { - Ok( - match lxd::container::get_info(&machine::machine_name(name))? { - None => PlatformStatus::NotInstalled, - Some(container) => { - if container.status == "Running" { - PlatformStatus::Running - } else { - PlatformStatus::Stopped - } - } - }, - ) - } - fn cmd(&self) -> impl CommandDriver { LxdCommandDriver { name: machine::machine_name(&self.name), } } -} - -/// "inspired" by lxd-rs -mod lxd { - use itertools::Itertools; - use serde::{Deserialize, Serialize}; - use std::path::Path; - use std::process::Output; - use std::{io, process::Command}; - - pub fn lxc(args: &[&str]) -> io::Result<()> { - let mut cmd = Command::new("lxc"); - cmd.arg("-q"); - for arg in args.iter() { - cmd.arg(arg); - } - - log::trace!("Running LXC command: {:?}", &cmd); - let status = cmd.spawn()?.wait()?; - if status.success() { - Ok(()) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - format!("LXD {:?} failed with {}", args, status), - )) - } + fn read_platform_status(name: &str, _: Private) -> Result { + Ok(lxd::container::get_platform_status( + &machine::machine_name(name), + )?) } - pub fn lxc_output(args: &[&str]) -> io::Result { - let mut cmd = Command::new("lxc"); - cmd.arg("-q"); - // see https://github.com/rust-lang/rust/issues/30098#issuecomment-160346319 - // cmd.stdin(Stdio::inherit()); - // cmd.stdout(Stdio::inherit()); - // cmd.stderr(Stdio::inherit()); - for arg in args.iter() { - cmd.arg(arg); - } - - log::trace!("Running LXC command: {:?}", &cmd); - - cmd.output() + fn install(&self, _: Private) -> Result<()> { + let lxd_name = machine_name(&self.name); + let rootfs = env::var("CODCHI_LXD_CONTAINER_MACHINE") + .map(|dir| PathBuf::from(dir)) + .context("Failed reading $CODCHI_LXD_CONTAINER_MACHINE from environment. This indicates a broken build.")?; + let mounts = vec![ + (host::DIR_NIX.join("store"), "/nix/store"), + ( + host::DIR_NIX.join("var/nix/daemon-socket"), + "/nix/var/nix/daemon-socket", + ), + (host::DIR_NIX.join("var/nix/db"), "/nix/var/nix/db"), + ( + host::DIR_CONFIG.join_machine(&self.name), + "/nix/var/nix/profiles", + ), + (host::DIR_CONFIG.clone(), "/nix/var/nix/profiles/codchi"), + (host::DIR_DATA.join_machine(&self.name), user::DEFAULT_HOME), + ]; + lxd::container::install(&lxd_name, &rootfs, mounts)?; + + Ok(()) } - pub fn lxc_output_ok(args: &[&str]) -> io::Result> { - let output = lxc_output(&args)?; - if output.status.success() { - Ok(output.stdout) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - format!("LXD {:?} failed with {}", args, output.status), - )) - } - } - - pub mod image { - use super::*; - pub fn import>(path: P, alias: &str) -> io::Result<()> { - lxc(&[ - "image", - "import", - &format!("{}", path.as_ref().display()), - "--alias", - alias, - ]) - } - - pub fn delete(name: &str) -> io::Result<()> { - lxc(&["image", "delete", name]) - } - - pub fn init(image_name: &str, container_name: &str) -> io::Result<()> { - lxc(&["init", image_name, container_name]) - } + fn start(&self, _: Private) -> Result<()> { + Ok(lxd::container::start(&machine_name(&self.name))?) } - pub mod container { - use super::*; - - #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] - /// LXD container information - pub struct Info { - pub status: String, - } - - pub fn start(name: &str) -> io::Result<()> { - lxc(&["start", name]) - } - - // pub fn stop(name: &str, force: bool) -> io::Result<()> { - // if force { - // lxc(&["stop", "--force", name]) - // } else { - // lxc(&["stop", name]) - // } - // } - - pub fn list(filter: &str) -> io::Result> { - let json = lxc_output_ok(&["list", filter, "--format", "json"])?; - serde_json::from_slice::>(&json).map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("LXD info: failed to parse json: {}", err), - ) - }) - } + fn force_stop(&self, _: Private) -> Result<()> { + Ok(lxd::container::stop(&machine_name(&self.name), true)?) + } - pub fn get_info(name: &str) -> io::Result> { - list(name).map(|mut list| { - if list.len() == 1 { - Some(list.remove(0)) - } else { - None - } - }) - } + fn delete_container(&self, _: Private) -> Result<()> { + Ok(lxd::container::delete(&machine_name(&self.name), true)?) + } +} - pub fn delete(name: &str, force: bool) -> io::Result<()> { - if force { - lxc(&["delete", name, "--force"]) - } else { - lxc(&["delete", name]) - } - } - pub fn config_set(name: &str, cfg: &str) -> io::Result<()> { - lxc(&["config", "set", name, cfg]) +pub struct LxdCommandDriver { + pub name: String, +} +impl CommandDriver for LxdCommandDriver { + fn build(&self, user: Option, cwd: Option) -> std::process::Command { + let mut cmd = std::process::Command::new("lxc"); + cmd.arg("-q"); + cmd.args(&["exec", &self.name]); + if let Some(cwd) = &cwd { + cmd.args(&["--cwd", &cwd]); } - - pub fn config_mount>( - container_name: &str, - disk_name: &str, - source: P, - dest: &str, - ) -> io::Result<()> { - lxc(&[ - "config", - "device", - "add", - container_name, - disk_name, - "disk", - &format!("source={}", source.as_ref().display()), - &format!("path={}", dest), - ]) + if *DEBUG { + cmd.args(&["--env", "CODCHI_DEBUG=1"]); + } + if let Some(user) = &user { + cmd.args(&[ + "--user", + match user { + LinuxUser::Root => consts::user::ROOT_UID, + LinuxUser::Default => consts::user::DEFAULT_UID, + }, + ]); + cmd.args(&[ + "--group", + match user { + LinuxUser::Root => consts::user::ROOT_GID, + LinuxUser::Default => consts::user::DEFAULT_GID, + }, + ]); + cmd.args(&[ + "--env", + &format!( + "HOME={}", + match user { + LinuxUser::Root => consts::user::ROOT_HOME, + LinuxUser::Default => consts::user::DEFAULT_HOME, + } + ), + ]); } - - // pub fn get_logs(name: &str) -> io::Result { - // let out = lxc_output_ok(&["console", name, "--show-log"])?; - // Ok(String::from_utf8_lossy(&out).lines().dropping(2).join("\n")) - // } + cmd.arg("--"); + cmd } } +impl NixDriver for LxdCommandDriver {} diff --git a/codchi/cli/src/platform/machine.rs b/codchi/cli/src/platform/machine.rs index 32c9da6f..0c91cf3a 100644 --- a/codchi/cli/src/platform/machine.rs +++ b/codchi/cli/src/platform/machine.rs @@ -1,27 +1,73 @@ -use std::fs; - -use anyhow::Result; -use spinoff::*; - -use super::private::Private; +use super::{private::Private, LinuxUser}; use crate::{ - config::{Config, MachineConfig}, - consts::{host, machine, store, ToPath}, - platform::{Command, CommandDriver, Driver, Store}, - util::UtilExt, + config::{Config, MachineConfig, MutableConfig}, + consts::{self, host, store, user, ToPath}, + platform::{self, Command, CommandDriver, Driver, Store}, + util::with_spinner, }; +use anyhow::{bail, Context, Result}; +use itertools::Itertools; +use platform::Output; +use std::{fs, thread, time::Duration}; pub trait MachineDriver: Sized { - fn read_platform(name: &str, _: Private) -> Result; fn cmd(&self) -> impl CommandDriver; - fn read(name: String, config: MachineConfig, _: Private) -> Result { - let platform_status = Self::read_platform(&name, Private)?; - let config_status = { + /// Read if container is running / stopped / not installed + fn read_platform_status(name: &str, _: Private) -> Result; + + /// Import and configure machine container + fn install(&self, _: Private) -> Result<()>; + + /// Start container + fn start(&self, _: Private) -> Result<()>; + + /// Kill container + fn force_stop(&self, _: Private) -> Result<()>; + + /// Delete container + fn delete_container(&self, _: Private) -> Result<()>; +} + +#[derive(Debug, Clone)] +pub struct Machine { + pub name: String, + pub config: MachineConfig, + pub config_status: ConfigStatus, + pub platform_status: PlatformStatus, +} + +/// The (NixOS) status of the machine configuration +#[derive(Debug, PartialEq, Eq, Clone, strum::EnumString, strum::Display)] +pub enum ConfigStatus { + /// Machine was added / configured, but not built and installed + NotInstalled, + + /// Machine was already built and installed but config has changed (flake.nix has changed) + Modified, + + /// Machine was already built and installed but updates are available (flake.lock has changed) + UpdatesAvailable, + + /// Machine is built, installed and up to date + UpToDate, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PlatformStatus { + NotInstalled, + Stopped, + Running, +} + +impl Machine { + pub fn update_status(mut self) -> Result { + self.platform_status = Self::read_platform_status(&self.name, Private)?; + self.config_status = { use ConfigStatus::*; - let machine_dir = host::DIR_CONFIG.join_machine(&name); - if platform_status == PlatformStatus::NotInstalled - || fs::metadata(machine_dir.join("profile")).is_err() + let machine_dir = host::DIR_CONFIG.join_machine(&self.name); + if self.platform_status == PlatformStatus::NotInstalled + || fs::symlink_metadata(machine_dir.join("system")).is_err() { NotInstalled } else { @@ -39,47 +85,93 @@ else fi "#, )) - .cwd(store::DIR_CONFIG.join_machine(&name)), + .cwd(store::DIR_CONFIG.join_machine(&self.name)), )? } }; - Ok(Machine { - name, + Ok(self) + } + pub fn read(name: &str, config: MachineConfig, _: Private) -> Result { + Self { + name: name.to_string(), config, - config_status, - platform_status, - }) + config_status: ConfigStatus::NotInstalled, + platform_status: PlatformStatus::NotInstalled, + } + .update_status() } - fn by_name(name: String) -> Result> { - match Config::read()?.machines.get(&name) { - Some(config) => Ok(Some(Machine::read(name, config.clone(), Private)?)), - None => Ok(None), + /// Returns Err if machine doesn't exist + pub fn by_name(name: &str) -> Result { + match Config::read()?.machines.get(name) { + Some(config) => Ok(Self::read(name, config.clone(), Private)?), + None => bail!("There is no machine with name '{name}'. List available machines with `codchi status`."), } } - fn list() -> Result> { + pub fn list() -> Result> { Config::read()? .machines .into_iter() - .map(|(name, config)| Machine::read(name, config, Private)) + .map(|(name, config)| Self::read(&name, config, Private)) .collect() } - fn write(name: &str, machine: &MachineConfig) -> Result<()> { - let machine_dir = host::DIR_CONFIG.join_machine(name); + pub fn write_flake(&self) -> Result<()> { + let machine_dir = host::DIR_CONFIG.join_machine(&self.name); machine_dir.get_or_create()?; - fs::write(&machine_dir.join("flake.nix"), machine.gen_flake())?; - - let mut spinner = Spinner::new_with_stream( - spinners::Dots, - "Initializing machine...", - Color::Blue, - spinoff::Streams::Stderr, - ); - Driver::store() - .cmd() - .run( + + let flake = { + let codchi_url = consts::CODCHI_FLAKE_URL; + let module_inputs = self + .config + .modules + .iter() + .enumerate() + .map(|(idx, url)| format!(r#" "{idx}".url = "{}";"#, url.to_nix_url())) + .join("\n"); + let driver = platform::NIXOS_DRIVER_NAME; + let nix_system = consts::NIX_SYSTEM; + let nixpkgs = if let Some(idx) = self.config.nixpkgs_from { + format!(r#"inputs."{idx}".inputs.nixpkgs"#) + } else { + "inputs.codchi.inputs.nixpkgs".to_string() + }; + let modules = self + .config + .modules + .iter() + .enumerate() + .map(|(idx, url)| { + format!( + r#" inputs."{idx}".{module_name}"#, + module_name = url.flake_attr.0 + ) + }) + .join("\n"); + format!( + r#"{{ + inputs = {{ + codchi.url = "{codchi_url}"; +{module_inputs} + }}; + outputs = inputs: {{ + nixosConfigurations.default = inputs.codchi.lib.codeMachine {{ + driver = "{driver}"; + system = "{nix_system}"; + nixpkgs = {nixpkgs}; + modules = [ +{modules} + ]; + }}; + }}; +}}"# + ) + }; + fs::write(&machine_dir.join("flake.nix"), flake)?; + + with_spinner("Initializing machine...", |_| { + Driver::store().cmd().run( Command::script(format!( /* bash */ r#" @@ -89,94 +181,151 @@ if [ ! -d .git ]; then fi "# )) - .cwd(store::DIR_CONFIG.join_machine(name)), + .cwd(store::DIR_CONFIG.join_machine(&self.name)), ) - .finally(|| spinner.clear())?; + })?; Ok(()) } - fn build(name: &str) -> Result<()> { - let mut spinner = Spinner::new_with_stream( - spinners::Dots, - format!("Building {}...", machine::machine_name(name)), - Color::Blue, - spinoff::Streams::Stderr, - ); - Driver::store() - .cmd() - .run( + pub fn build(&self) -> Result<()> { + self.write_flake()?; + with_spinner(format!("Building {}...", self.name), |spinner| { + Driver::store().cmd().run( Command::script(format!( /* bash */ r#" -if [ ! -e profile ]; then - nix profile install --profile profile '.#nixosConfigurations.default.config.system.build.toplevel' +if [ ! -e system ]; then + nix profile install --profile system '.#nixosConfigurations.default.config.system.build.toplevel' else - nix profile upgrade --profile profile '.*' + nix profile upgrade --profile system '.*' fi +pwd git add flake.* "# )) - .cwd(store::DIR_CONFIG.join_machine(name)), - ) - .finally(|| spinner.clear())?; + .cwd(store::DIR_CONFIG.join_machine(&self.name)), + )?; - Ok(()) + spinner.update_text(format!("Building {}...", self.name)); + + let status = Self::read_platform_status(&self.name, Private)?; + if status == PlatformStatus::NotInstalled { + spinner.update_text(format!("Installing {}...", self.name)); + self.install(Private)?; + + spinner.update_text(format!("Initializing {}...", self.name)); + self.wait_online()?; + self.cmd().run(Command::new("poweroff", &[]))?; + } else { + if status == PlatformStatus::Stopped { + spinner.update_text(format!("Starting {}...", self.name)); + self.start(Private)?; + self.wait_online()?; + } + self.cmd().run( + Command::new( + "/nix/var/nix/profiles/system/bin/switch-to-configuration", + &["switch"], + ) + .user(LinuxUser::Root), + )?; + } + Ok(()) + }) } - fn update(name: &str) -> Result<()> { - let mut spinner = Spinner::new_with_stream( - spinners::Dots, - format!("Checking for updates for {}...", machine::machine_name(name)), - Color::Blue, - spinoff::Streams::Stderr, - ); - Driver::store() + pub fn wait_online(&self) -> Result<()> { + while self .cmd() - .run( - Command::script(format!( - /* bash */ - r#" -nix flake update -"# - )) - .cwd(store::DIR_CONFIG.join_machine(name)), + .run(Command::new("nix", &["store", "ping", "--store", "daemon"])) + .is_err() + { + thread::sleep(Duration::from_millis(250)); + } + Ok(()) + } + + pub fn update(self) -> Result { + self.write_flake()?; + with_spinner(format!("Checking for updates for {}...", self.name), |_| { + Driver::store().cmd().run( + Command::new("nix", &["flake", "update"]) + .cwd(store::DIR_CONFIG.join_machine(&self.name)), ) - .finally(|| spinner.clear())?; + })?; - Ok(()) + self.update_status() } -} -#[derive(Debug, Clone)] -pub struct Machine { - pub name: String, - pub config: MachineConfig, - pub config_status: ConfigStatus, - pub platform_status: PlatformStatus, -} + pub fn delete(self, im_really_sure: bool) -> Result<()> { + let name = &self.name; + if !im_really_sure + && !inquire::Confirm::new(&format!("Delete '{name}'?",)) + .with_help_message(&format!( + "This will remove all files associated with '{name}'" + )) + .prompt()? + { + bail!("Canceled deletion."); + } -/// The (NixOS) status of the machine configuration -#[derive(Debug, PartialEq, Eq, Clone, strum::EnumString, strum::Display)] -pub enum ConfigStatus { - /// Machine was added / configured, but not built and installed - NotInstalled, + with_spinner("", |spinner| { + spinner.update_text(format!("Stopping {name}")); + if self.platform_status == PlatformStatus::Running { + self.force_stop(Private)?; + } + spinner.update_text(format!("Deleting container of {name}")); + if self.platform_status != PlatformStatus::NotInstalled { + MachineDriver::delete_container(&self, Private)?; + } - /// Machine was already built and installed but config has changed (flake.nix has changed) - Modified, + spinner.update_text(format!("Deleting files from {name}")); + Driver::store() + .cmd() + .run(Command::new( + "rm", + &[ + "-rf", + store::DIR_DATA.join_machine(&self.name).to_str().unwrap(), + store::DIR_CONFIG.join_machine(&self.name).to_str().unwrap(), + ], + )) + .context("Failed deleting data.")?; - /// Machine was already built and installed but updates are available (flake.lock has changed) - UpdatesAvailable, + let mut cfg = MutableConfig::open()?; + cfg.get_machines().remove_entry(&name); + cfg.write()?; - /// Machine is built, installed and up to date - UpToDate, - // Machine is currently building / installing - // Installing, -} + Ok(()) + }) + } -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum PlatformStatus { - NotInstalled, - Stopped, - Running, + pub fn exec(&self, cmd: &[String]) -> Result<()> { + if self.config_status == ConfigStatus::NotInstalled + || self.platform_status == PlatformStatus::NotInstalled + { + bail!( + "Machine {} wasn't installed yet. Install with `codchi rebuild {}`.", + self.name, + self.name + ); + } + + if self.platform_status == PlatformStatus::Stopped { + self.start(Private)?; + self.wait_online()?; + } + + let cmd = match cmd.split_first() { + Some((cmd, args)) => { + Command::new(cmd, &args.iter().map(|str| str.as_str()).collect_vec()) + .user(LinuxUser::Default) + } + None => Command::new("su", &["-l", user::DEFAULT_NAME]), + }; + self.cmd() + .exec(cmd.output(Output::Inherit).cwd(user::DEFAULT_HOME))?; + Ok(()) + } } diff --git a/codchi/cli/src/platform/store.rs b/codchi/cli/src/platform/store.rs index c3ea6edd..e94d704a 100644 --- a/codchi/cli/src/platform/store.rs +++ b/codchi/cli/src/platform/store.rs @@ -25,28 +25,27 @@ pub trait Store: Sized { r#"{{ inputs.codchi.url = "{CODCHI_FLAKE_URL}"; outputs = {{ codchi, ... }}: {{ - packages.{NIX_SYSTEM}.default = codchi.packages.{NIX_SYSTEM}.{NIX_STORE_PACKAGE}.config.system.build.runtime; + packages.{NIX_SYSTEM}.default = codchi.packages.{NIX_SYSTEM}.{NIX_STORE_PACKAGE}.config.build.runtime; }}; }}"# ); fs::write(flake_path, flake_content)?; - with_spinner("Starting store container...", |spinner| { - spinner.update_after_time( - "Starting store container. This may take a while the first time...", - Duration::from_secs(10), - ); - let store = Self::start_or_init_container(private::Private)?; - - while store - .cmd() - .run(Command::new("nix", &["store", "ping", "--store", "daemon"])) - .is_err() - { - thread::sleep(Duration::from_millis(250)); - } - Ok(store) - }) + with_spinner( + "Starting store container. This may take a while the first time...", + |_| { + let store = Self::start_or_init_container(private::Private)?; + + while store + .cmd() + .run(Command::new("nix", &["store", "ping", "--store", "daemon"])) + .is_err() + { + thread::sleep(Duration::from_millis(250)); + } + Ok(store) + }, + ) } /// Get driver for running commands inside store diff --git a/codchi/default.nix b/codchi/default.nix index e53a3869..1f3a7a41 100644 --- a/codchi/default.nix +++ b/codchi/default.nix @@ -9,6 +9,7 @@ , lib , store-lxd-tarball +, machine-lxd-tarball , platform , makeRustPlatform @@ -178,7 +179,8 @@ let # targetCargo = "X86_64-UNKNOWN-LINUX-GNU"; CARGO_BUILD_TARGET = "x86_64-unknown-linux-gnu"; - CODCHI_LXD_CTRL_ROOTFS = store-lxd-tarball; + CODCHI_LXD_CONTAINER_STORE = store-lxd-tarball; + CODCHI_LXD_CONTAINER_MACHINE = machine-lxd-tarball; passthru = { inherit xwin; diff --git a/codchi/shell.nix b/codchi/shell.nix index a12339d8..aac25f2a 100644 --- a/codchi/shell.nix +++ b/codchi/shell.nix @@ -4,6 +4,7 @@ , codchi +, nixd , nixpkgs-fmt , strace , gdb @@ -27,7 +28,7 @@ let shellHook = codchi.passthru.setupXWin "$(git rev-parse --show-toplevel)"; }; linux = { - inherit (codchi) CODCHI_LXD_CTRL_ROOTFS; + inherit (codchi) CODCHI_LXD_CONTAINER_STORE CODCHI_LXD_CONTAINER_MACHINE; LD_LIBRARY_PATH = lib.makeLibraryPath codchi.buildInputs; }; }.${platform}; @@ -39,9 +40,11 @@ mkShell (lib.recursiveUpdate inputsFrom = [ codchi ]; packages = [ + nixd + nixpkgs-fmt + codchi.passthru.rust codchi.passthru.nix-git - nixpkgs-fmt strace gdb gdbgui @@ -63,6 +66,7 @@ mkShell (lib.recursiveUpdate ${codchi.passthru.xwin}/bin/xwin --accept-license --cache-dir "$CACHE" download cat "$CACHE"/dl/manifest*.json '') + ] ++ (codchi.nativeBuildInputs or [ ]); shellHook = '' diff --git a/flake.nix b/flake.nix index fe870602..8457c526 100644 --- a/flake.nix +++ b/flake.nix @@ -16,30 +16,33 @@ inherit system; overlays = [ (import rust-overlay) - (self: _: - let - - mkStore = driver: (import ./nix/store - { - inherit inputs; - inherit (nixpkgs) lib; - pkgs = self; - } - { - config.driver.${driver}.enable = true; - } - ); - in - { - codchi = self.callPackage ./codchi { inherit (inputs) self; platform = "linux"; }; - codchi-windows = self.callPackage ./codchi { inherit (inputs) self; platform = "win"; }; - - store-lxd = mkStore "lxd"; - store-lxd-tarball = self.store-lxd.config.system.build.tarball; - store-wsl = mkStore "wsl"; - store-wsl-tarball = self.store-wsl.config.system.build.tarball; - }) - (import ./nix/pkgs) + (self: _: { + codchi = self.callPackage ./codchi { inherit (inputs) self; platform = "linux"; }; + codchi-windows = self.callPackage ./codchi { inherit (inputs) self; platform = "win"; }; + + mkContainer = type: driver: (import ./nix/container + { + inherit inputs; + inherit (nixpkgs) lib; + pkgs = self; + } + { + config.${type} = { + enable = true; + driver.${driver}.enable = true; + }; + } + ); + store-lxd = self.mkContainer "store" "lxd"; + store-lxd-tarball = self.store-lxd.config.build.tarball; + store-wsl = self.mkContainer "store" "wsl"; + store-wsl-tarball = self.store-wsl.config.build.tarball; + + machine-lxd = self.mkContainer "machine" "lxd"; + machine-lxd-tarball = self.machine-lxd.config.build.tarball; + machine-wsl = self.mkContainer "machine" "wsl"; + machine-wsl-tarball = self.machine-wsl.config.build.tarball; + }) ]; config.allowUnfree = true; }; @@ -56,10 +59,11 @@ { inherit lib; - nixosModules.default = import ./nix/modules; + nixosModules.default = import ./nix/nixos; nixosModules.codchi = { nixpkgs.config.allowUnfree = true; environment.systemPackages = [ pkgs.vscodium ]; + programs.neovim.enable = true; programs.direnv = { enable = true; nix-direnv.enable = true; @@ -67,7 +71,7 @@ }; packages.${system} = { - inherit (pkgs) store-lxd store-wsl; + inherit (pkgs) store-lxd store-wsl machine-lxd machine-wsl; default = pkgs.codchi; windows = pkgs.codchi-windows; }; @@ -114,8 +118,8 @@ flip mapAttrs exampleModules (_: module: lib.codeMachine { inherit system driver nixpkgs; - specialArgs.inputs = inputs; - codchiModules = [{ inherit module; }]; + # specialArgs.inputs = inputs; + modules = [ module ]; }); in { diff --git a/nix/container/consts.nix b/nix/container/consts.nix new file mode 100644 index 00000000..24ebffb5 --- /dev/null +++ b/nix/container/consts.nix @@ -0,0 +1,19 @@ +{ consts, ... }: { + + _module.args.consts = { + store = { + DIR_CONFIG = "/config"; + DIR_CONFIG_STORE = "${consts.store.DIR_CONFIG}/store"; + PROFILE_STORE = "${consts.store.DIR_CONFIG_STORE}/profile"; + DIR_CONFIG_MACHINE = "${consts.store.DIR_CONFIG}/machine/$CODCHI_MACHINE_NAME"; + + DIR_DATA = "/data"; + DIR_DATA_MACHINE = "${consts.store.DIR_DATA}/machine/$CODCHI_MACHINE_NAME"; + }; + machine = { + USER = "codchi"; + }; + }; + + +} diff --git a/nix/container/default.nix b/nix/container/default.nix new file mode 100644 index 00000000..a12fe2e2 --- /dev/null +++ b/nix/container/default.nix @@ -0,0 +1,24 @@ +{ lib, pkgs, inputs }: +{ config, specialArgs ? { } }: + +lib.evalModules { + inherit specialArgs; + modules = [ + + { + _module.args = { + inherit inputs; + pkgs = pkgs.extend (import ./pkgs); + }; + } + + ./module.nix + ./consts.nix + + ./store + ./machine + + config + + ]; +} diff --git a/nix/container/machine/default.nix b/nix/container/machine/default.nix new file mode 100644 index 00000000..17878108 --- /dev/null +++ b/nix/container/machine/default.nix @@ -0,0 +1,110 @@ +{ pkgs, lib, config, ... }: +let + inherit (lib) mkOption mkEnableOption types mkIf; + cfg = config.machine; +in +{ + imports = [ + ./lxd.nix + ./wsl.nix + ]; + + options.machine = { + enable = mkEnableOption "machine container"; + init = + let + mkInitStage = description: mkOption { + type = types.lines; + description = '' + ${description} + + Use `lib.mkBefore` / `lib.mkAfter` to inject scripts before / after. + ''; + default = ""; + }; + in + { + hostSetup = mkInitStage "Create directories and mount host directories."; + }; + }; + + config = mkIf cfg.enable { + + name = "machine"; + + binPackages = with pkgs.pkgsStatic; [ + busybox + bashInteractive + + (pkgs.writeLoginShellScriptBinStatic "run" /* bash */ '' + exec "$@" + '') + + (pkgs.writeLoginShellScriptBinStatic "runin" /* bash */ '' + source <(cat -) + '') + ]; + + # modeled after /nixos/modules/system/boot/stage-2-init.sh + files."/sbin/init" = pkgs.writeShellScriptStatic "init" + /* bash */ + '' + set -euo pipefail + + if [ -n "''${CODCHI_DEBUG:-}" ]; then + set -x + fi + + # temporarily store host's $PATH (useful for WSL) + export HOST_PATH="$PATH" + # Use config.system.binPackages and PATH from host + export LANG="C.UTF-8" HOME=/root PATH="/bin:$PATH" + + # Log the script output to /dev/kmsg or /run/log/stage-2-init.log. + # Only at this point are all the necessary prerequisites ready for these commands. + exec {logOutFd}>&1 {logErrFd}>&2 + if test -w /dev/kmsg; then + exec > >(tee -i /proc/self/fd/"$logOutFd" | while read -r line; do + if test -n "$line"; then + echo "<7>stage-2-init: $line" > /dev/kmsg + fi + done) 2>&1 + else + mkdir -p /run/log + exec > >(tee -i /run/log/stage-2-init.log) 2>&1 + fi + + ${cfg.init.hostSetup} + + # Required by the activation script + install -m 0755 -d /etc /etc/nixos + install -m 01777 -d /tmp + + # Run the script that performs all configuration activation that does + # not have to be done at boot time. + echo "running activation script..." + /nix/var/nix/profiles/system/activate + + # Append host's $PATH with lower priority than NixOS' $PATH + export PATH="$PATH:$HOST_PATH" + + # Record the boot configuration. + ln -sfn "$(readlink -f /nix/var/nix/profiles/system)" /run/booted-system + + # Ensure systemd doesn't try to populate /etc, by forcing its first-boot + # heuristic off. It doesn't matter what's in /etc/machine-id for this purpose, + # and systemd will immediately fill in the file when it starts, so just + # creating it is enough. This `: >>` pattern avoids forking and avoids changing + # the mtime if the file already exists. + : >> /etc/machine-id + + # Reset the logging file descriptors. + exec 1>&$logOutFd 2>&$logErrFd + exec {logOutFd}>&- {logErrFd}>&- + + echo "starting systemd..." + exec /run/current-system/systemd/lib/systemd/systemd "$@" + ''; + }; + +} diff --git a/nix/container/machine/lxd.nix b/nix/container/machine/lxd.nix new file mode 100644 index 00000000..2ba3879d --- /dev/null +++ b/nix/container/machine/lxd.nix @@ -0,0 +1,32 @@ +{ lib, config, pkgs, ... }: +let inherit (lib) mkEnableOption mkIf; +in +{ + + options.machine.driver.lxd = { + enable = mkEnableOption "LXD specific settings"; + }; + + config = mkIf config.machine.driver.lxd.enable { + build.tarballExtraCommands = /* bash */ '' + DIRNAME="$(pwd)" + cd .. + mv "$DIRNAME" rootfs + mkdir "$DIRNAME" + mv rootfs "$DIRNAME" + cd "$DIRNAME" + + cat < metadata.yaml + ${builtins.toJSON { + architecture = builtins.elemAt (builtins.match "^([a-z0-9_]+).+" (toString pkgs.system)) 0; + creation_date = 1; + # properties = { + # description = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} ${pkgs.system}"; + # os = "${config.system.nixos.distroId}"; + # release = "${config.system.nixos.codeName}"; + # }; + }} + EOF + ''; + }; +} diff --git a/nix/container/machine/wsl.nix b/nix/container/machine/wsl.nix new file mode 100644 index 00000000..38c1d516 --- /dev/null +++ b/nix/container/machine/wsl.nix @@ -0,0 +1,58 @@ +{ lib, config, pkgs, consts, ... }: +let inherit (lib) mkEnableOption mkIf pipe concatLines; + + mnt = "/mnt/wsl/codchi"; + # WSL needs imperative mounting. Order is important! + mounts = [ + { target = "/nix/store"; source = "/nix/store"; } + { target = "/nix/var/nix/daemon-socket"; source = "/nix/var/nix/daemon-socket"; } + { target = "/nix/var/nix/db"; source = "/nix/var/nix/db"; } + { target = "/nix/var/nix/profiles"; source = consts.store.DIR_CONFIG_MACHINE; } + # keep all profiles for GC + { target = "/nix/var/nix/profiles/codchi"; source = consts.store.DIR_CONFIG; } + { target = "/home/${consts.machine.USER}"; source = consts.store.DIR_DATA_MACHINE; } + ]; + + mkMounts = pipe mounts [ + (map ({ target, source }: + let realSrc = mnt + source; + in /* bash */ '' + [ ! -d "${target}" ] && mkdir -p "${target}" + mount --bind "${realSrc}" "${target}" + '')) + concatLines + ]; + +in +{ + + options.machine.driver.wsl = { + enable = mkEnableOption "WSL specific settings"; + }; + + config = mkIf config.machine.driver.wsl.enable { + + # defaults omitted (see https://learn.microsoft.com/en-us/windows/wsl/wsl-config) + files."etc/wsl.conf" = pkgs.writeText "wsl.conf" (lib.generators.toINI { } { + automount.mountFsTab = false; + automount.options = "metadata,uid=1000,gid=100"; # TODO is this needed? + user.default = consts.machine.USER; + boot.systemd = true; + }); + + machine.init.hostSetup = /* bash */ '' + if [ -z "''${CODCHI_MACHINE_NAME:-}" ]; then + echo "This distribution is only meant to be started by codchi.exe!" + exit 1 + fi + + ${mkMounts} + + if [ -n "''${CODCHI_WSL_USE_VCXSRV:-}" ]; then + export PULSE_SERVER=tcp:$(ip route | awk '/^default/{print $3; exit}'); + export DISPLAY=$(ip route | awk '/^default/{print $3; exit}'):0 + unset WAYLAND_DISPLAY + fi + ''; + }; +} diff --git a/nix/store/system.nix b/nix/container/module.nix similarity index 50% rename from nix/store/system.nix rename to nix/container/module.nix index a46bab1b..330cdc21 100644 --- a/nix/store/system.nix +++ b/nix/container/module.nix @@ -1,17 +1,14 @@ -{ config, lib, pkgs, inputs, ... }: +{ config, lib, pkgs, ... }: let inherit (lib) types mkOption; - - cfg = config.system; in { - options.system = { + options = { name = mkOption { type = types.str; description = '' - Name of this store system. + Name of this container. ''; - default = "store"; }; binPackages = mkOption { type = with types; listOf package; @@ -27,15 +24,15 @@ in default = [ ]; example = lib.literalExpression "[ pkgs.git pkgs.openssh ]"; description = lib.mdDoc '' - The set of packages that are installed to the nix store. They're - installed via `nix profile install`. + The set of packages that are installed to the nix store. They must be + installed manually (e.g. via `nix profile install` / `environment.systemPackages`). ''; }; files = mkOption { type = with types; attrsOf (nullOr path); description = lib.mdDoc '' - Set of files that have to be copied to {file}`/`. Deactivate a file by - setting it to {nix}`null`. + Set of files that have to be copied to `/`. Deactivate a file by + setting it to `null`. ''; default = { }; example = lib.literalExpression '' @@ -75,61 +72,24 @@ in }; - config.system = { - binPackages = with pkgs.pkgsStatic; [ - busybox - bashInteractive - nix - ]; - - runtimePackages = with pkgs; [ - coreutils - # iputils - git - openssh - ]; - - files = { - # user & groups required for minimal linux + `nix daemon` - "/etc/group" = ./etc/group; - "/etc/passwd" = ./etc/passwd; - # required for dns / other information lookup systems (mainly glibc) - "/etc/nsswitch.conf" = ./etc/nsswitch.conf; - # nix settings - "/etc/nix/nix.conf" = ./etc/nix/nix.conf; - # force the nix registry to use the nixpkgs version from this repo - "/etc/nix/registry.json" = pkgs.writeText "registry.json" (builtins.toJSON { - version = 2; - flakes = [{ - exact = true; - from = { type = "indirect"; id = "nixpkgs"; }; - to = { type = "path"; path = inputs.nixpkgs.outPath; } - // lib.filterAttrs - (n: _: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash") - inputs.nixpkgs; - }]; - }); + config.build = { + runtime = pkgs.buildEnv { + name = "runtime-env"; + pathsToLink = [ "/bin" ]; + paths = config.runtimePackages; }; - - build = { - runtime = pkgs.buildEnv { - name = "runtime-env"; - pathsToLink = [ "/bin" ]; - paths = cfg.runtimePackages; - }; - tarball = pkgs.makeTarball { - fileName = cfg.name; - contents = [ - (pkgs.buildEnvCopy { - name = "bin"; - pathsToLink = [ "/bin" ]; - paths = cfg.binPackages; - }) - (pkgs.buildHierarchy cfg.files) - ]; - extraCommands = config.system.build.tarballExtraCommands; - }; + tarball = pkgs.makeTarball { + fileName = config.name; + contents = [ + (pkgs.buildEnvCopy { + name = "bin"; + pathsToLink = [ "/bin" ]; + paths = config.binPackages; + }) + (pkgs.buildHierarchy config.files) + ]; + extraCommands = config.build.tarballExtraCommands; }; - }; + } diff --git a/nix/pkgs/build-hierarchy.nix b/nix/container/pkgs/build-hierarchy.nix similarity index 100% rename from nix/pkgs/build-hierarchy.nix rename to nix/container/pkgs/build-hierarchy.nix diff --git a/nix/pkgs/buildenv-copy.nix b/nix/container/pkgs/buildenv-copy.nix similarity index 100% rename from nix/pkgs/buildenv-copy.nix rename to nix/container/pkgs/buildenv-copy.nix diff --git a/nix/pkgs/default.nix b/nix/container/pkgs/default.nix similarity index 70% rename from nix/pkgs/default.nix rename to nix/container/pkgs/default.nix index 289ed266..949ea599 100644 --- a/nix/pkgs/default.nix +++ b/nix/container/pkgs/default.nix @@ -32,4 +32,18 @@ _self: super: { meta.mainProgram = name; }; + writeLoginShellScriptBinStatic = name: text: super.writeTextFile { + inherit name; + executable = true; + destination = "/bin/${name}"; + text = '' + #!/bin/bash -l + ${text} + ''; + checkPhase = '' + ${super.stdenv.shellDryRun} "$target" + ''; + meta.mainProgram = name; + }; + } diff --git a/nix/pkgs/make-tarball.nix b/nix/container/pkgs/make-tarball.nix similarity index 100% rename from nix/pkgs/make-tarball.nix rename to nix/container/pkgs/make-tarball.nix diff --git a/nix/container/store/default.nix b/nix/container/store/default.nix new file mode 100644 index 00000000..81483a92 --- /dev/null +++ b/nix/container/store/default.nix @@ -0,0 +1,206 @@ +{ inputs, pkgs, lib, config, consts, ... }: +let + inherit (lib) mkOption mkEnableOption types mkIf; + cfg = config.store; +in +{ + imports = [ + ./lxd.nix + ./wsl.nix + ]; + + options.store = { + enable = mkEnableOption "store container"; + init = + let + mkInitStage = description: mkOption { + type = types.lines; + description = '' + ${description} + + Use `lib.mkBefore` / `lib.mkAfter` to inject scripts before / after. + ''; + default = ""; + }; + in + { + filesystem = mkInitStage "Create directories and mount host directories."; + ssl = mkInitStage "Setup SSL certs."; + runtime = mkInitStage "Install / update stores' runtime dependencies via nix."; + files = mkInitStage "Create / update static files."; + services = mkInitStage '' + Start service in the background. This must not terminate before the + last service (nix-daemon). + ''; + }; + }; + + config = mkIf cfg.enable (lib.mkMerge [ + { + + name = "store"; + + runtimePackages = with pkgs; [ + coreutils + git + openssh + ]; + + build.shellInit = /* bash */ '' + set -euo pipefail + + if [ -n "''${CODCHI_DEBUG:-}" ]; then + set -x + fi + + # Use config.system.binPackages and PATH from parent + export PATH="/bin:${consts.store.PROFILE_STORE}/bin:/root/.nix-profile/bin:$PATH" + + # Ensure a consistent umask. + umask 0022 + + # Make nixs' https work + export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + ''; + + + files = { + # user & groups required for minimal linux + `nix daemon` + "/etc/group" = ./etc/group; + "/etc/passwd" = ./etc/passwd; + # required for dns / other information lookup systems (mainly glibc) + "/etc/nsswitch.conf" = ./etc/nsswitch.conf; + # nix settings + "/etc/nix/nix.conf" = ./etc/nix/nix.conf; + # force the nix registry to use the nixpkgs version from this repo + "/etc/nix/registry.json" = pkgs.writeText "registry.json" (builtins.toJSON { + version = 2; + flakes = [{ + exact = true; + from = { type = "indirect"; id = "nixpkgs"; }; + to = { type = "path"; path = inputs.nixpkgs.outPath; } + // lib.filterAttrs + (n: _: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash") + inputs.nixpkgs; + }]; + }); + "/sbin/init" = pkgs.writeShellScriptStatic "init" (with cfg.init; lib.concatLines [ + /* bash */ + '' + ${config.build.shellInit} + + logE() { + echo "$@" >&2 + } + '' + filesystem + ssl + runtime + files + services + ]); + }; + + binPackages = with pkgs.pkgsStatic; [ + busybox + bashInteractive + nix + + (pkgs.writeShellScriptBinStatic "run" /* bash */ '' + ${config.build.shellInit} + + exec "$@" + '') + + (pkgs.writeShellScriptBinStatic "runin" /* bash */ '' + ${config.build.shellInit} + + source <(cat -) + '') + ]; + } + + { + + store.init.filesystem = lib.concatLines ( + map + (dir: "[ -d /${dir} ] || mkdir /${dir}") + [ "dev" "nix" "proc" "sys" "tmp" "var" ] + ); + + } + + { + # add official ca certificates to enable https + files."/etc/ssl/certs/nix.crt" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + + # TODO append custom certs to main bundle + store.init.ssl = /* bash */ '' + if [ -f "${consts.store.DIR_DATA}/certs/system.crt" ]; then + cp -f "${consts.store.DIR_DATA}/certs/system.crt" /etc/ssl/certs/ca-certificates.crt + else + ln -fs /etc/ssl/certs/nix.crt /etc/ssl/certs/ca-certificates.crt + fi + ''; + } + { + + store.init.runtime = /* bash */ '' + if [ ! -f "${consts.store.DIR_CONFIG_STORE}/flake.nix" ]; then + logE "Stores' flake.nix missing!" + exit 1 + fi + _GIT_IMPURE= + if ! command -v git &> /dev/null; then + # git is needed for first `nix profile install` from flake + _GIT_IMPURE=1 + mkdir -p "${consts.store.DIR_CONFIG_STORE}" + nix profile install nixpkgs#git + fi + if [ ! -d "${consts.store.DIR_CONFIG_STORE}/.git" ]; then + logE "Initializing store..." + ( cd "${consts.store.DIR_CONFIG_STORE}" + git init -q + git add flake.* + ) + fi + if [ -n "$(git -C "${consts.store.DIR_CONFIG_STORE}" diff)" ]; then + logE "Checking for updates..." + ( cd "${consts.store.DIR_CONFIG_STORE}" + nix flake update + git add flake.* + ) + fi + if [ -n "$_GIT_IMPURE" ]; then + logE "Installing store..." + mkdir -p "${consts.store.DIR_CONFIG_STORE}" + nix profile install --profile "${consts.store.PROFILE_STORE}" "${consts.store.DIR_CONFIG_STORE}" + + # remove impure git from default profile + nix profile remove '.*' + nix profile wipe-history + else + logE "Updating store..." + nix profile upgrade --profile "${consts.store.PROFILE_STORE}" '.*' + fi + ''; + } + + ( + let program = config.build.tarball.passthru.createFiles; + in { + runtimePackages = [ program ]; + store.init.files = /* bash */ '' + ${program.meta.mainProgram} + ''; + } + ) + + { + store.init.services = lib.mkAfter /* bash */ '' + nix daemon + ''; + } + ]); + +} diff --git a/nix/store/etc/group b/nix/container/store/etc/group similarity index 100% rename from nix/store/etc/group rename to nix/container/store/etc/group diff --git a/nix/store/etc/nix/nix.conf b/nix/container/store/etc/nix/nix.conf similarity index 100% rename from nix/store/etc/nix/nix.conf rename to nix/container/store/etc/nix/nix.conf diff --git a/nix/store/etc/nsswitch.conf b/nix/container/store/etc/nsswitch.conf similarity index 100% rename from nix/store/etc/nsswitch.conf rename to nix/container/store/etc/nsswitch.conf diff --git a/nix/store/etc/passwd b/nix/container/store/etc/passwd similarity index 100% rename from nix/store/etc/passwd rename to nix/container/store/etc/passwd diff --git a/nix/store/etc/wsl.conf b/nix/container/store/etc/wsl.conf similarity index 100% rename from nix/store/etc/wsl.conf rename to nix/container/store/etc/wsl.conf diff --git a/nix/store/lxd.nix b/nix/container/store/lxd.nix similarity index 92% rename from nix/store/lxd.nix rename to nix/container/store/lxd.nix index e7ed8030..ee7f2afa 100644 --- a/nix/store/lxd.nix +++ b/nix/container/store/lxd.nix @@ -17,14 +17,14 @@ let inherit (lib) mkEnableOption mkIf; in { - options.driver.lxd = { + options.store.driver.lxd = { enable = mkEnableOption "LXD specific settings"; }; - config.system = mkIf config.driver.lxd.enable { + config = mkIf config.store.driver.lxd.enable { files."/usr/share/udhcpc/default.script" = udhcpcScript; # these should automatically fork to bg - init.filesystem = lib.mkAfter /* bash */ '' + store.init.filesystem = lib.mkAfter /* bash */ '' syslogd udhcpc -S -s /usr/share/udhcpc/default.script ''; diff --git a/nix/store/wsl.nix b/nix/container/store/wsl.nix similarity index 66% rename from nix/store/wsl.nix rename to nix/container/store/wsl.nix index 2b7ae5e4..dceb1fe3 100644 --- a/nix/store/wsl.nix +++ b/nix/container/store/wsl.nix @@ -2,20 +2,25 @@ let inherit (lib) mkEnableOption mkIf; in { - options.driver.wsl = { + options.store.driver.wsl = { enable = mkEnableOption "WSL specific settings"; }; - config = mkIf config.driver.wsl.enable { - system.files."etc/wsl.conf" = pkgs.writeText "wsl.conf" (lib.generators.toINI { } { + config = mkIf config.store.driver.wsl.enable { + files."etc/wsl.conf" = pkgs.writeText "wsl.conf" (lib.generators.toINI { } { automount.mountFsTab = false; boot.command = "/bin/run /sbin/init"; }); - system.init.filesystem = lib.mkAfter /* bash */ '' + store.init.filesystem = lib.mkAfter /* bash */ '' + if [ -z "$CODCHI_IS_STORE" ]; then + echo "This distribution is only meant to be started by codchi.exe!" + exit 1 + fi + ${with lib; pipe { - ${consts.DIR_CONFIG} = "$WSL_CODCHI_DIR_CONFIG"; - ${consts.DIR_DATA} = "$WSL_CODCHI_DIR_DATA"; + ${consts.store.DIR_CONFIG} = "$WSL_CODCHI_DIR_CONFIG"; + ${consts.store.DIR_DATA} = "$WSL_CODCHI_DIR_DATA"; } [ (mapAttrsToList (path: var: /* bash */ '' diff --git a/nix/lib.nix b/nix/lib.nix index 3fd9fd12..f5ca2a40 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -32,7 +32,7 @@ rec { codeMachine = { # driver :: null | "wsl" | "lxd" - # null is for migrating away from codchi + # TODO null is for migrating away from codchi driver # system :: "x86_64-linux" | "aarch64-linux" , system @@ -40,17 +40,20 @@ rec { , nixpkgs # modules :: [{ module :: NixOS Module, extraArgs :: Attr Set }] , modules - , ... + # , ... }: nixpkgs.lib.nixosSystem { inherit system; modules = - map - # TODO consider adding specialArgs of other codchiModules for example as global.NAME - ({ module, specialArgs ? { } }: overrideModuleArgs specialArgs module) - ([{ module = ./modules; specialArgs.inputs.nixpkgs = nixpkgs; }] - ++ modules) + # map + # # TODO consider adding specialArgs of other codchiModules for example as global.NAME + # ({ module, specialArgs ? { } }: overrideModuleArgs specialArgs module) + # ([{ module = ./nixos; specialArgs.inputs.nixpkgs = nixpkgs; }] ++ modules) + modules ++ [ + ./nixos + { _module.args.inputs = { inherit nixpkgs; }; } + ] ++ - nixpkgs.lib.optional (driver != null) { codchi.internal.${driver}.enable = true; } + nixpkgs.lib.optional (driver != null) { codchi.driver.${driver}.enable = true; } ; }; diff --git a/nix/modules/default.nix b/nix/modules/default.nix deleted file mode 100644 index 45b6ef44..00000000 --- a/nix/modules/default.nix +++ /dev/null @@ -1,68 +0,0 @@ -{ inputs, lib, config, pkgs, ... }: -let - inherit (lib) mkOption mkDefault types; - cfg = config.codchi; -in -{ - - imports = [ - ./docker.nix - ./internal - ./java.nix - ./recommended-config.nix - ]; - - options.codchi = { - defaultUser = mkOption { - type = types.str; - readOnly = true; - default = "nixos"; - description = '' - The name of the default linux user. This can be used to configure user - specific things (e.g. add an user to a group) via `''${config.codchi.defaultUser}` - ''; - }; - }; - - config = { - # disable nixos-rebuild - system.disableInstallerTools = true; - - environment.variables.NIX_REMOTE = "daemon"; - systemd.services = { - nix-daemon.enable = false; - nix-gc.enable = false; - nix-optimize.enable = false; - }; - - # Setup nix flakes - nix = { - package = pkgs.nixFlakes; - extraOptions = '' - experimental-features = nix-command flakes - ''; - registry.nixpkgs.flake = inputs.nixpkgs; - nixPath = [ "nixpkgs=/etc/channels/nixpkgs" ]; - }; - environment.etc."channels/nixpkgs".source = inputs.nixpkgs; - - # User stuff - users.users.${cfg.defaultUser} = { - isNormalUser = true; - uid = 1000; - extraGroups = [ "wheel" ]; - initialPassword = "nixos"; - }; - - # The default user will not have a password by default - security.sudo.wheelNeedsPassword = mkDefault false; - - # Desktop stuff - environment.systemPackages = [ - pkgs.xdg-utils - pkgs.nixos-icons # needed for gnome and pantheon about dialog, nixos-manual and maybe more - ]; - fonts.enableDefaultFonts = true; - }; - -} diff --git a/nix/modules/internal/default.nix b/nix/modules/internal/default.nix deleted file mode 100644 index 7a73ec27..00000000 --- a/nix/modules/internal/default.nix +++ /dev/null @@ -1,49 +0,0 @@ -{ pkgs, ... }: -{ - imports = [ - ./rootfs.nix - - ./lxd - ./wsl - ]; - - config = { - - # Copy .desktop files and corresponding icons (converted to .ico) to - # /run/current-system/sw/share/codchi. We copy instead of symlink them so - # that drivers like WSL can see them. - environment.extraSetup = '' - pushd $out/share - - mkdir -p codchi/{icos,applications} - - for app_path in applications/*.desktop; do - if grep -q ^NoDisplay=true "$app_path" || grep -q ^Hidden=true "$app_path"; then - echo "Skipping hidden $app_path" >&2 - else - APP_NAME="$(basename "$app_path" | sed 's/\.desktop//')" - ICON_NAME="$(grep Icon "$app_path" | sed 's/Icon=//')" - DIRS= - [ -d icons ] && DIRS="$DIRS icons" - [ -d pixmaps ] && DIRS="$DIRS pixmaps" - ICON_PATH="$(find -L $DIRS -name "$ICON_NAME.*" | sort -rV | head -n1)" - if [ ! -z "$ICON_PATH" ]; then - ${pkgs.imagemagick}/bin/convert \ - -background transparent \ - -define icon:auto-resize=16,24,32,48,64,72,96,128,256 \ - "$ICON_PATH" "codchi/icos/$APP_NAME.ico" - fi - cp "$app_path" codchi/applications - fi - done - - popd - ''; - - # Make sure all profiles are recorded as gcroots - system.activationScripts."add-gcroots" = '' - mkdir -p /nix/var/nix/gcroots - ln -fs /nix/var/nix/profiles /nix/var/nix/gcroots/ - ''; - }; -} diff --git a/nix/modules/internal/lxd/default.nix b/nix/modules/internal/lxd/default.nix deleted file mode 100644 index bb7a83dd..00000000 --- a/nix/modules/internal/lxd/default.nix +++ /dev/null @@ -1,75 +0,0 @@ -{ pkgs, config, lib, ... }: -let - inherit (lib) mapAttrs' nameValuePair - mkEnableOption mkIf; - - cfg = config.codchi.internal.lxd; -in -{ - options.codchi.internal.lxd.enable = mkEnableOption "codchi's LXD driver" - // { internal = true; readonly = true; }; - - config = mkIf cfg.enable { - - codchi.internal.init = { - rootfsContents = { - "/bin/" = with pkgs.pkgsStatic; toString (map (pkg: "${pkg}/bin/*") [ busybox bash ]); - - "/sbin/init" = pkgs.writeScript "sytemd-wrapper" '' - #!/bin/bash - set -e - - LANG="C.UTF-8" /nix/var/nix/profiles/system/activate - exec /nix/var/nix/profiles/system/systemd/lib/systemd/systemd "$@" - ''; - - "/etc/" = null; - "/dev/" = null; - "/proc/" = null; - "/sys/" = null; - "/tmp" = null; - "/var" = null; - }; - - overrideContents = rootfs: - # prepend /rootfs - mapAttrs' (path: nameValuePair ("/rootfs" + path)) rootfs - // { - "/metadata.yaml" = (pkgs.formats.yaml {}).generate "metadata.yaml" { - architecture = builtins.elemAt (builtins.match "^([a-z0-9_]+).+" (toString pkgs.system)) 0; - creation_date = 1; - # properties = { - # description = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} ${pkgs.system}"; - # os = "${config.system.nixos.distroId}"; - # release = "${config.system.nixos.codeName}"; - # }; - }; - }; - }; - - boot.isContainer = true; - - # Add the overrides from lxd distrobuilder - # https://github.com/lxc/distrobuilder/blob/05978d0d5a72718154f1525c7d043e090ba7c3e0/distrobuilder/main.go#L630 - systemd.packages = [ - (pkgs.writeTextFile { - name = "systemd-lxc-service-overrides"; - destination = "/etc/systemd/system/service.d/zzz-lxc-service.conf"; - text = '' - [Service] - ProcSubset=all - ProtectProc=default - ProtectControlGroups=no - ProtectKernelTunables=no - NoNewPrivileges=no - LoadCredential= - ''; - }) - ]; - - # Allow the user to login as root without password. - # users.users.root.initialHashedPassword = lib.mkOverride 150 ""; - - }; -} - diff --git a/nix/modules/internal/rootfs.nix b/nix/modules/internal/rootfs.nix deleted file mode 100644 index ebed3922..00000000 --- a/nix/modules/internal/rootfs.nix +++ /dev/null @@ -1,88 +0,0 @@ -{ config, lib, pkgs, ... }: -let - inherit (lib) types - literalExpression mdDoc stringAfter; - mkOption = args: lib.mkOption ({ internal = true; } // args); - rootfsType = with types; attrsOf (nullOr (oneOf [ str package ])); - - cfg = config.codchi.internal; - - rootfs = pkgs.callPackage ../../nix/make-tarball.nix { - # this filename ($DRV/rootfs.tar) is used by codchi during instance - # registration with a driver - fileName = "rootfs"; - inherit (cfg.init) overrideContents; - contents = cfg.init.rootfsContents; - compressCommand = "cat"; - compressionExtension = ""; - }; -in -{ - options = { - codchi.internal = { - init = { - rootfsContents = mkOption { - type = rootfsType; - description = mdDoc '' - Files which are copied into the tarball (when building - `config.system.build.tarball`) or into a code machine during - activation. - - Attribute name is the target path which will be created if non - existant. Directories must be given with a trailing `/`. - Attribute value can be either `null` or a space separated - list of store paths (which can include globs) which are copied to the - target directory. - ''; - example = literalExpression ''{ - "/etc/hosts" = pkgs.writeText "hosts" ...; - - # creates empty /dev - "/dev/" = null; - - # creates /bin with binaries from static busybox and bash - "/bin/" = "''${pkgs.pkgsStatic.busybox}/bin/* ''${pkgs.pkgsStatic.bash}/bin/*"; - }''; - }; - overrideContents = mkOption { - type = types.functionTo rootfsType; - default = x: x; - description = mdDoc '' - Function which takes `codchi.internal.init.rootfsContents` and - return the contents of the tarball which is installed by a codchi - driver. This can be used to add metadata or other files which are - needed by a particular driver. - ''; - }; - }; - }; - system.build.codchi.rootfs = mkOption { - type = types.package; - readOnly = true; - description = mdDoc '' - Derivation which can be installed by codchi. Contains following files: - - - `./rootfs.tar`: the code machine rootfs - - `./system-store-path`: a text file with the store path to - `config.system.build.toplevel` - ''; - }; - }; - - config = { - - # Sync files required before NixOS boots (files from - # codchi.internal.init.rootfsContents) - system.activationScripts.codchi-rootfs = stringAfter [ "etc" ] '' - pushd / - ${rootfs.passthru.createContents}/bin/create-contents - popd - ''; - - system.build.codchi.rootfs = pkgs.runCommandLocal "codchi-instance-rootfs" { } '' - cp -a ${rootfs} $out - chmod +w $out - echo ${config.system.build.toplevel} > $out/system-store-path - ''; - }; -} diff --git a/nix/modules/internal/wsl/default.nix b/nix/modules/internal/wsl/default.nix deleted file mode 100644 index 4114cdcf..00000000 --- a/nix/modules/internal/wsl/default.nix +++ /dev/null @@ -1,116 +0,0 @@ -{ config, pkgs, lib, ... }: -let - inherit (lib) mkEnableOption mkIf - generators; - cfg = config.codchi.internal.wsl; -in -{ - - imports = [ - ./driver.nix - ./default-tools.nix - ]; - - options.codchi.internal.wsl.enable = mkEnableOption "codchi's WSL driver" - // { internal = true; readonly = true; }; - - config = mkIf cfg.enable { - - codchi.internal.init.rootfsContents = { - "/bin/" = with pkgs.pkgsStatic; toString (map (pkg: "${pkg}/bin/*") [ busybox bash ]); - - # See https://learn.microsoft.com/en-us/windows/wsl/wsl-config#configuration-settings-for-wslconf for all options - "/etc/wsl.conf" = pkgs.writeText "wsl.conf" (generators.toINI { } { - automount = { - enabled = true; - mountFsTab = false; - root = "/mnt"; - options = "metadata,uid=1000,gid=100"; - }; - boot = { - command = ""; - systemd = true; - initPath = "/sbin/init"; - initWaitCommand = pkgs.writeShellScript "nixos-wsl-init-wait" '' - ${pkgs.systemd}/bin/systemctl is-system-running | ${pkgs.gnugrep}/bin/grep -E "(running|degraded)" - exit $? - ''; - initShutdownCommand = pkgs.writeShellScript "nixos-wsl-init-shutdown" '' - ${pkgs.systemd}/bin/systemctl reboot "$@" - ''; - }; - network = { - generateHosts = true; - generateResolvConf = true; - hostname = config.networking.hostName; - }; - interop = { - enabled = true; - appendWindowsPath = true; - }; - user.default = config.codchi.defaultUser; - }); - - "/sbin/init" = pkgs.writeScript "sytemd-wrapper" '' - #!/bin/bash - set -e - set -o pipefail - # this logs everything to dmesg - set -x - - PATH="/bin:$PATH" - - if [ ! -d /mnt/wsl/nix/store ] || [ ! -d /mnt/wsl/nix/var/nix/daemon-socket ] ; then - echo "Remote store is not mounted on /mnt/wsl/nix" >&2 - exit 1 - fi - if [ ! -S /mnt/wsl/nix/var/nix/daemon-socket/socket ] ; then - echo "Remote nix-daemon is not running." >&2 - exit 1 - fi - - mnt() { - echo "Mounting remote $1 on $2..." - mkdir -p "$2" || true - mount --bind "/mnt/wsl/$1" "$2" $3 - - trap "umount -f \"$2\"" EXIT - } - - mnt /nix/store /nix/store -r - mnt /nix/var/nix/daemon-socket /nix/var/nix/daemon-socket - mnt "/nix/var/nix/profiles/per-instance/$WSL_DISTRO_NAME" /nix/var/nix/profiles - - mnt /nix/var/nix/profiles /nix/var/nix/profiles/global - mnt /nix/var/nix/db /nix/var/nix/db - - LANG="C.UTF-8" /nix/var/nix/profiles/system/activate - exec /nix/var/nix/profiles/system/systemd/lib/systemd/systemd "$@" - ''; - - }; - - environment = { - variables = { - AAAA_WSLPATH = "$PATH"; # Use as' as a prefix which is sorted alphabetically before PATH in a nix attrset so the generated export statement isn't overriden by the NixOS PATH - LIBGL_ALWAYS_INDIRECT = "1"; # Allow OpenGL in WSL - DONT_PROMPT_WSL_INSTALL = "1"; # Don't prompt for VS code server when running `code` - - # https://wiki.archlinux.org/title/Java#Better_font_rendering - JDK_JAVA_OPTIONS = "-Dawt.useSystemAAFontSettings=on, -Dswing.aatext=true"; - }; - extraInit = '' - # include Windows' $PATH in our $PATH - export PATH="$PATH:$AAAA_WSLPATH" - unset AAAA_WSLPATH - - export PULSE_SERVER=tcp:$(ip route | awk '/^default/{print $3; exit}'); - export DISPLAY=$(ip route | awk '/^default/{print $3; exit}'):0 - unset WAYLAND_DISPLAY - ''; - - }; - - }; - -} diff --git a/nix/modules/internal/wsl/driver.nix b/nix/modules/internal/wsl/driver.nix deleted file mode 100644 index 4b195dae..00000000 --- a/nix/modules/internal/wsl/driver.nix +++ /dev/null @@ -1,74 +0,0 @@ -{ config, lib, pkgs, ... }: -let - inherit (lib) mkIf mkRemovedOptionModule stringAfter; - - cfg = config.codchi.internal.wsl; -in -{ - - imports = [ - (mkRemovedOptionModule [ "wsl" "docker-native" ] "WSL specific docker options are no longer supported. Please use `virtualisation.docker` directly.") - (mkRemovedOptionModule [ "wsl" "docker-desktop" ] "WSL specific options are no longer supported.") - ]; - - config = mkIf cfg.enable { - - system.activationScripts = { - setup-wsl = stringAfter [ "etc" ] '' - ln -sf /init /bin/wslpath - ''; - }; - - # Allow executing .exe files from codchi - boot.binfmt.registrations = { - WSLInterop = { - magicOrExtension = "MZ"; - fixBinary = true; - wrapInterpreterInShell = false; - interpreter = "/init"; - preserveArgvZero = true; - }; - }; - - # WSL uses its own kernel and boot loader - boot = { - initrd.enable = false; - kernel.enable = false; - loader.grub.enable = false; - modprobeConfig.enable = false; - }; - system.build.installBootLoader = "${pkgs.coreutils}/bin/true"; - console.enable = false; # WSL does not support virtual consoles - hardware.opengl.enable = true; # Enable GPU acceleration - - environment.etc = { - hosts.enable = false; - "resolv.conf".enable = false; - }; - - networking.dhcpcd.enable = false; # dhcp is handled by windows - - # Otherwise WSL fails to login as root with "initgroups failed 5" - users.users.root.extraGroups = [ "root" ]; - - powerManagement.enable = false; - - # useful for usbip but adds a dependency on various firmwares which are combined over 300 MB big - services.udev.enable = lib.mkDefault false; - - # Disable systemd units that don't make sense on WSL - systemd = { - services = { - firewall.enable = false; - systemd-resolved.enable = lib.mkDefault false; - # systemd-timesyncd actually works in WSL and without it the clock can drift - systemd-timesyncd.unitConfig.ConditionVirtualization = ""; - }; - - enableEmergencyMode = false; # Don't allow emergency mode, because we don't have a console. - oomd.enable = false; - }; - }; - - -} diff --git a/nix/nixos/default.nix b/nix/nixos/default.nix new file mode 100644 index 00000000..fbdcaf4d --- /dev/null +++ b/nix/nixos/default.nix @@ -0,0 +1,7 @@ +{ + imports = [ + ../container/consts.nix + ./driver + ./modules + ]; +} diff --git a/nix/nixos/driver/default.nix b/nix/nixos/driver/default.nix new file mode 100644 index 00000000..fd0c6b72 --- /dev/null +++ b/nix/nixos/driver/default.nix @@ -0,0 +1,145 @@ +{ inputs, lib, pkgs, config, consts, ... }: +let + inherit (lib) mkOption types mkDefault mkForce mkMerge; +in +{ + imports = [ + ./lxd + ./wsl + ]; + + options.codchi.driver.name = mkOption { + type = types.str; + internal = true; + }; + + config = mkMerge [ + + # Nix(OS) stuff managed by codchi + { + system.build.codchi.container = (import ../../container { inherit inputs pkgs lib; } + { + config.machine = { + enable = true; + driver.${config.codchi.driver.name}.enable = true; + }; + }).config.build.tarball; + # Create files required by the driver + system.activationScripts.codchi-create-files = lib.stringAfter [ "etc" ] /* bash */ '' + ( cd / && + ${lib.getExe config.system.build.codchi.container.passthru.createFiles} + ) + ''; + + # Make sure all profiles are recorded as gcroots + systemd.tmpfiles.rules = [ + "L+ /nix/var/nix/gcroots/profiles 0755 root root - /nix/var/nix/profiles" + ]; + + # disable nixos-rebuild + system.disableInstallerTools = mkForce true; + + systemd = { + services = { + nix-daemon.enable = mkForce false; + nix-gc.enable = mkForce false; + nix-optimize.enable = mkForce false; + }; + sockets.nix-daemon.enable = mkForce false; + }; + + environment.variables.NIX_REMOTE = "daemon"; + # Setup nix flakes + nix = { + package = pkgs.nixFlakes; + extraOptions = '' + experimental-features = nix-command flakes + ''; + registry.nixpkgs.flake = inputs.nixpkgs; + nixPath = [ "nixpkgs=/etc/channels/nixpkgs" ]; + }; + environment.etc."channels/nixpkgs".source = inputs.nixpkgs; + } + + + # general codchi machine management + { + boot.isContainer = true; + + # useful for usbip but adds a dependency on various firmwares which are combined over 300 MB big + services.udev.enable = mkDefault false; + + networking.firewall.enable = mkDefault false; + + systemd = { + # Don't allow emergency mode, because we don't have a console. + enableEmergencyMode = false; + # systemd-oomd requires cgroup pressure info which WSL doesn't have + oomd.enable = false; + }; + + users.mutableUsers = mkForce false; + users.users.${consts.machine.USER} = { + isNormalUser = mkForce true; + createHome = mkForce true; + home = mkForce "/home/${consts.machine.USER}"; + uid = mkForce 1000; + extraGroups = [ "wheel" ]; + initialPassword = consts.machine.USER; + }; + security.sudo.wheelNeedsPassword = mkDefault false; + } + + # Desktop stuff + { + xdg = mkDefault { + autostart.enable = true; + menus.enable = true; + mime.enable = true; + icons.enable = true; + }; + fonts.enableDefaultPackages = mkDefault true; + + environment = { + + systemPackages = [ + pkgs.xdg-utils + pkgs.nixos-icons # needed for gnome and pantheon about dialog, nixos-manual and maybe more + ]; + + # Copy .desktop files and corresponding icons (converted to .ico) to + # /run/current-system/sw/share/codchi. We copy instead of symlink them so + # that drivers like WSL can see them. + extraSetup = /* bash */ '' + pushd $out/share + + mkdir -p codchi/{icos,applications} + + for app_path in applications/*.desktop; do + if grep -q ^NoDisplay=true "$app_path" || grep -q ^Hidden=true "$app_path"; then + echo "Skipping hidden $app_path" >&2 + else + APP_NAME="$(basename "$app_path" | sed 's/\.desktop//')" + ICON_NAME="$(grep Icon "$app_path" | sed 's/Icon=//')" + DIRS= + [ -d icons ] && DIRS="$DIRS icons" + [ -d pixmaps ] && DIRS="$DIRS pixmaps" + ICON_PATH="$(find -L $DIRS -name "$ICON_NAME.*" | sort -rV | head -n1)" + if [ ! -z "$ICON_PATH" ]; then + ${pkgs.imagemagick}/bin/convert \ + -background transparent \ + -define icon:auto-resize=16,24,32,48,64,72,96,128,256 \ + "$ICON_PATH" "codchi/icos/$APP_NAME.ico" + fi + cp "$app_path" codchi/applications + fi + done + + popd + ''; + }; + + } + ]; + +} diff --git a/nix/nixos/driver/lxd/default.nix b/nix/nixos/driver/lxd/default.nix new file mode 100644 index 00000000..40775326 --- /dev/null +++ b/nix/nixos/driver/lxd/default.nix @@ -0,0 +1,52 @@ +{ pkgs, config, lib, ... }: +let + inherit (lib) mkEnableOption mkIf; + + cfg = config.codchi.driver.lxd; +in +{ + options.codchi.driver.lxd.enable = mkEnableOption "LXD driver" + // { internal = true; readonly = true; }; + + config = mkIf cfg.enable { + + codchi.driver.name = "lxd"; + + # Somehow setfacl -m fails during switch-to-configuration but succeedes on + # container boot, so we mark them to only run on boot + environment.etc."tmpfiles.d/systemd.conf".source = + lib.mkForce (pkgs.runCommandLocal "lxd-setfacl-m-fix.systemd.conf" { } '' + cp "${pkgs.systemd}/example/tmpfiles.d/systemd.conf" "$out" + + sed -i "s|a+ /var/log/journal|# OVERRIDDEN to setfacl -m on boot only\na+! /var/log/journal|" "$out" + ''); + + # Add the overrides from lxd distrobuilder + # https://github.com/lxc/distrobuilder/blob/05978d0d5a72718154f1525c7d043e090ba7c3e0/distrobuilder/main.go#L630 + systemd.packages = [ + (pkgs.writeTextFile { + name = "systemd-lxc-service-overrides"; + destination = "/etc/systemd/system/service.d/zzz-lxc-service.conf"; + text = '' + [Service] + ProcSubset=all + ProtectProc=default + ProtectControlGroups=no + ProtectKernelTunables=no + NoNewPrivileges=no + LoadCredential= + ''; + + # Additional settings for privileged containers + # ProtectHome=no + # ProtectSystem=no + # PrivateDevices=no + # PrivateTmp=no + # ProtectKernelLogs=no + # ProtectKernelModules=no + # ReadWritePaths= + }) + ]; + }; +} + diff --git a/nix/modules/internal/wsl/default-tools.nix b/nix/nixos/driver/wsl/default-tools.nix similarity index 97% rename from nix/modules/internal/wsl/default-tools.nix rename to nix/nixos/driver/wsl/default-tools.nix index b34c0cd5..748ac8be 100644 --- a/nix/modules/internal/wsl/default-tools.nix +++ b/nix/nixos/driver/wsl/default-tools.nix @@ -1,7 +1,7 @@ { config, pkgs, lib, ... }: let inherit (lib) mkIf; - cfg = config.codchi.internal.wsl; + cfg = config.codchi.driver.wsl; in { config = mkIf cfg.enable { @@ -60,9 +60,8 @@ in }; wslExplorer = pkgs.writeShellScriptBin "wsl-explorer" '' - set -e - set -o pipefail - explorer.exe "$(wslpath -w "$1")" + set -euo pipefail + exec explorer.exe "$(wslpath -w "$1")" ''; }; diff --git a/nix/nixos/driver/wsl/default.nix b/nix/nixos/driver/wsl/default.nix new file mode 100644 index 00000000..db2ceafe --- /dev/null +++ b/nix/nixos/driver/wsl/default.nix @@ -0,0 +1,71 @@ +{ config, pkgs, lib, ... }: +let + inherit (lib) mkEnableOption mkIf; + cfg = config.codchi.driver.wsl; +in +{ + + imports = [ ./default-tools.nix ]; + + options.codchi.driver.wsl.enable = mkEnableOption "WSL driver" + // { internal = true; readonly = true; }; + + config = mkIf cfg.enable { + + codchi.driver.name = "wsl"; + + environment.etc = { + hosts.enable = false; + "resolv.conf".enable = false; + }; + networking.dhcpcd.enable = false; # dhcp is handled by windows + + # Otherwise WSL fails to login as root with "initgroups failed 5" + # TODO check if still issue with systemd + users.users.root.extraGroups = [ "root" ]; + + # Allow executing .exe files from WSL + boot.binfmt.registrations = { + WSLInterop = { + magicOrExtension = "MZ"; + fixBinary = true; + wrapInterpreterInShell = false; + interpreter = "/init"; + preserveArgvZero = true; + }; + }; + + # prevent clockshift + services.timesyncd.enable = true; + systemd.services.systemd-timesyncd.unitConfig.ConditionVirtualization = ""; + + systemd.tmpfiles.settings = { + # Link the X11 socket into place. This is a no-op on a normal setup, + # but helps if /tmp is a tmpfs or mounted from some other location. + "10-wslg-x11" = { + "/tmp/.X11-unix" = { + L = { + argument = "/mnt/wslg/.X11-unix"; + }; + }; + }; + "11-wslpath" = { + "/bin/wslpath" = { + L = { + argument = "/init"; + }; + }; + }; + }; + + environment.variables = { + LIBGL_ALWAYS_INDIRECT = "1"; # Allow OpenGL in WSL + DONT_PROMPT_WSL_INSTALL = "1"; # Don't prompt for VS code server when running `code` + + # https://wiki.archlinux.org/title/Java#Better_font_rendering TODO include by default? + JDK_JAVA_OPTIONS = "-Dawt.useSystemAAFontSettings=on, -Dswing.aatext=true"; + }; + + }; + +} diff --git a/nix/nixos/modules/default.nix b/nix/nixos/modules/default.nix new file mode 100644 index 00000000..1f0f2c2d --- /dev/null +++ b/nix/nixos/modules/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + + ]; +} diff --git a/nix/modules/docker.nix b/nix/nixos/modules/docker.nix similarity index 100% rename from nix/modules/docker.nix rename to nix/nixos/modules/docker.nix diff --git a/nix/modules/java.nix b/nix/nixos/modules/java.nix similarity index 100% rename from nix/modules/java.nix rename to nix/nixos/modules/java.nix diff --git a/nix/modules/recommended-config.nix b/nix/nixos/modules/recommended-config.nix similarity index 80% rename from nix/modules/recommended-config.nix rename to nix/nixos/modules/recommended-config.nix index 7f890171..5e9860b9 100644 --- a/nix/modules/recommended-config.nix +++ b/nix/nixos/modules/recommended-config.nix @@ -14,9 +14,6 @@ in bzip2 perl ]; - - programs.command-not-found.enable = false; - programs.nix-index.enable = true; }; } diff --git a/nix/store/default.nix b/nix/store/default.nix deleted file mode 100644 index 9bfd7386..00000000 --- a/nix/store/default.nix +++ /dev/null @@ -1,30 +0,0 @@ -{ lib, pkgs, inputs }: -{ config, specialArgs ? { } }: - -lib.evalModules { - inherit specialArgs; - modules = [ - config - - ({ consts, ... }: { - _module.args = { - inherit pkgs inputs; - - # TODO unify variables with rust cli - consts = { - DIR_CONFIG = "/config"; - DIR_DATA = "/data"; - DIR_CONFIG_STORE = "${consts.DIR_CONFIG}/store"; - PROFILE_STORE = "${consts.DIR_CONFIG_STORE}/profile"; - }; - }; - }) - - ./scripts.nix - ./system.nix - - ./lxd.nix - ./wsl.nix - - ]; -} diff --git a/nix/store/scripts.nix b/nix/store/scripts.nix deleted file mode 100644 index b98280b1..00000000 --- a/nix/store/scripts.nix +++ /dev/null @@ -1,182 +0,0 @@ -{ pkgs, lib, config, consts, ... }: -let - - inherit (lib) mkOption types; - cfg = config.system; - -in -{ - - options.system = { - extraActivation = mkOption { - type = types.lines; - description = lib.mdDoc '' - Shell scripts which are executed every time a process is launched in - store. Therefore they must run very quick (at least most of the time). - - Only `config.system.binPackages` (busybox, nix without git or ssh, and - bash) are available here. - - Use `lib.mkBefore` / `lib.mkAfter` to influence script order. - ''; - default = ""; - }; - init = - let - mkInitStage = description: mkOption { - type = types.lines; - description = '' - ${description} - - Use `lib.mkBefore` / `lib.mkAfter` to inject scripts before / after. - ''; - default = ""; - }; - in - { - filesystem = mkInitStage "Create directories and mount host directories."; - ssl = mkInitStage "Setup SSL certs."; - runtime = mkInitStage "Install / update stores' runtime dependencies via nix."; - files = mkInitStage "Create / update static files."; - services = mkInitStage '' - Start service in the background. This must not terminate before the - last service (nix-daemon). - ''; - }; - }; - - config.system = lib.mkMerge [ - - { - build.shellInit = /* bash */ '' - set -euo pipefail - - # Use config.system.binPackages and PATH from parent - export PATH="/bin:${consts.PROFILE_STORE}/bin:/root/.nix-profile/bin:$PATH" - - # Ensure a consistent umask. - umask 0022 - - # Make nixs' http work - export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt - - # extra activation scripts - ${cfg.extraActivation} - ''; - - binPackages = [ - (pkgs.writeShellScriptBinStatic "run" /* bash */ '' - ${config.system.build.shellInit} - - exec "$@" - '') - - (pkgs.writeShellScriptBinStatic "runin" /* bash */ '' - ${config.system.build.shellInit} - - source <(cat -) - '') - ]; - - files."/sbin/init" = pkgs.writeShellScriptStatic "init" (with cfg.init; lib.concatLines [ - /* bash */ - '' - ${config.system.build.shellInit} - - logE() { - echo "$@" >&2 - } - '' - filesystem - ssl - runtime - files - services - ]); - } - - { - - init.filesystem = lib.concatLines ( - map - (dir: "[ -d /${dir} ] || mkdir /${dir}") - [ "dev" "nix" "proc" "sys" "tmp" "var" ] - ); - - } - - { - # add official ca certificates to enable https - files."/etc/ssl/certs/nix.crt" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; - - # TODO append custom certs to main bundle - init.ssl = /* bash */ '' - if [ -f "${consts.DIR_DATA}/certs/system.crt" ]; then - cp -f "${consts.DIR_DATA}/certs/system.crt" /etc/ssl/certs/ca-certificates.crt - else - ln -fs /etc/ssl/certs/nix.crt /etc/ssl/certs/ca-certificates.crt - fi - ''; - } - { - - init.runtime = /* bash */ '' - if [ ! -f "${consts.DIR_CONFIG_STORE}/flake.nix" ]; then - logE "Stores' flake.nix missing!" - exit 1 - fi - _GIT_IMPURE= - if ! command -v git &> /dev/null; then - # git is needed for first `nix profile install` from flake - _GIT_IMPURE=1 - mkdir -p "${consts.DIR_CONFIG_STORE}" - nix profile install nixpkgs#git - fi - if [ ! -d "${consts.DIR_CONFIG_STORE}/.git" ]; then - logE "Initializing store..." - ( cd "${consts.DIR_CONFIG_STORE}" - git init -q - git add flake.* - ) - fi - if [ -n "$(git -C "${consts.DIR_CONFIG_STORE}" diff)" ]; then - logE "Checking for updates..." - ( cd "${consts.DIR_CONFIG_STORE}" - nix flake update - git add flake.* - ) - fi - if [ -n "$_GIT_IMPURE" ]; then - logE "Installing store..." - mkdir -p "${consts.DIR_CONFIG_STORE}" - nix profile install --profile "${consts.PROFILE_STORE}" "${consts.DIR_CONFIG_STORE}" - - # remove impure git from default profile - nix profile remove '.*' - nix profile wipe-history - else - logE "Updating store..." - nix profile upgrade --profile "${consts.PROFILE_STORE}" '.*' - fi - ''; - } - - ( - let program = config.system.build.tarball.passthru.createFiles; - in { - runtimePackages = [ program ]; - init.files = /* bash */ '' - ${program.meta.mainProgram} - ''; - } - ) - - { - init.services = lib.mkAfter /* bash */ '' - nix daemon - ''; - - } - ]; - -}