diff --git a/flake.nix b/flake.nix index dc653271..a45019f3 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Manage system config using nix on any distro"; + description = "Manage system configurations using Nix on any Linux distribution."; nixConfig = { extra-substituters = [ "https://numtide.cachix.org" ]; @@ -90,5 +90,21 @@ }); } ); + + nixosModules = rec { + system-manager = ./nix/modules; + default = system-manager; + }; + + templates = { + standalone = { + path = ./templates/standalone; + description = "System Manager standalone setup"; + }; + nixos = { + path = ./templates/nixos; + description = "System Manager as a NixOS module"; + }; + }; }; } diff --git a/package.nix b/package.nix index 4c73ed0f..3c9e57e1 100644 --- a/package.nix +++ b/package.nix @@ -23,6 +23,7 @@ let ./Cargo.lock ./src ./test/rust + ./templates ]; }; diff --git a/shell.nix b/shell.nix index c80f3847..5ae34da5 100644 --- a/shell.nix +++ b/shell.nix @@ -40,5 +40,6 @@ pkgs.mkShellNoCC { clippy mdbook mdformat + rust-analyzer ]; } diff --git a/src/main.rs b/src/main.rs index 8e629843..2cff4157 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,27 @@ -use anyhow::Result; +use anyhow::{anyhow, bail, Result}; use clap::Parser; use std::ffi::OsString; +use std::fs::{create_dir_all, OpenOptions}; +use std::io::Write; use std::mem; use std::path::{Path, PathBuf}; use std::process::{self, ExitCode}; use system_manager::{NixOptions, StorePath}; +/// The bytes for the NixOS flake template is included in the binary to avoid unnecessary +/// network calls when initializing a system-manager configuration from the command line. +pub const NIXOS_FLAKE_TEMPLATE: &[u8; 683] = include_bytes!("../templates/nixos/flake.nix"); + +/// The bytes for the standalone flake template is included in the binary to avoid unnecessary +/// network calls when initializing a system-manager configuration from the command line. +pub const STANDALONE_FLAKE_TEMPLATE: &[u8; 739] = + include_bytes!("../templates/standalone/flake.nix"); + +/// The bytes for the standalone module template is included in the binary to avoid unnecessary +/// network calls when initializing a system-manager configuration from the command line. +pub const SYSTEM_MODULE_TEMPLATE: &[u8; 1153] = include_bytes!("../templates/system.nix"); + #[derive(clap::Parser, Debug)] #[command( author, @@ -31,6 +46,34 @@ struct Args { nix_options: Option>, } +#[derive(clap::Args, Debug)] +struct InitArgs { + /// The path to initialize the configuration at. + #[arg( + // The default_value is not resolved at this point so we must + // parse it ourselves with a value_parser closure. + default_value = "~/.config/system-manager", + value_parser = |src: &str| -> Result { + if src.starts_with("~") { + if let Some(home) = std::env::home_dir() { + let expanded = src.replace("~", &home.to_string_lossy()); + return Ok(PathBuf::from(expanded)); + } + bail!("Failed to determine a home directory to initialize the configuration in.") + } + Ok(PathBuf::from(src)) + }, + )] + path: PathBuf, + /// Whether or not to include a 'flake.nix' as part of the new configuration. + /// By default, if the host has the 'flakes' and 'nix-command' experimental features + /// enabled, a 'flake.nix' will be included. A flake template is automatically selected + /// by checking the host system's features. Flake templates are available on the system-manager + /// flake attribute 'outputs.templates'. + #[arg(long, default_value = "false")] + no_flake: bool, +} + #[derive(clap::Args, Debug)] struct BuildArgs { #[arg(long = "flake", name = "FLAKE_URI")] @@ -83,6 +126,13 @@ struct StoreOrFlakeArgs { #[derive(clap::Subcommand, Debug)] enum Action { + /// Initializes a configuration in the given directory. If the directory + /// does not exist, then it will be created. The default directory is + /// '~/.config/system-manager'. + Init { + #[command(flatten)] + init_args: InitArgs, + }, /// Build a new system-manager generation, register it as the active profile, and activate it Switch { #[command(flatten)] @@ -177,6 +227,60 @@ fn go(args: Args) -> Result<()> { &nix_options, ) .and_then(print_store_path), + Action::Init { + init_args: InitArgs { mut path, no_flake }, + } => { + create_dir_all(&path).map_err(|err| { + anyhow!( + "encountered an error while creating configuration directory '{}': {err:?}", + path.display() + ) + })?; + path = path.canonicalize().map_err(|err| { + anyhow!( + "failed to resolve '{}' into an absolute path: {err:?}", + path.display() + ) + })?; + log::info!( + "Initializing new system-manager configuration at '{}'", + path.display() + ); + + let system_config_filepath = { + let mut buf = path.clone(); + buf.push("system.nix"); + buf + }; + init_config_file(&system_config_filepath, SYSTEM_MODULE_TEMPLATE)?; + + let has_flake_support = process::Command::new("nix") + .arg("show-config") + .output() + .is_ok_and(|output| { + let out_str = String::from_utf8_lossy(&output.stdout); + out_str.contains("experimental-features") + && out_str.contains("flakes") + && out_str.contains("nix-command") + }); + if !no_flake && has_flake_support { + let flake_config_filepath = { + let mut buf = path.clone(); + buf.push("flake.nix"); + buf + }; + let is_nixos = process::Command::new("nixos-version") + .output() + .is_ok_and(|output| !output.stdout.is_empty()); + if is_nixos { + init_config_file(&flake_config_filepath, NIXOS_FLAKE_TEMPLATE)? + } else { + init_config_file(&flake_config_filepath, STANDALONE_FLAKE_TEMPLATE)? + } + } + log::info!("Configuration '{}' ready for activation!", path.display()); + Ok(()) + } Action::Switch { build_args: BuildArgs { flake_uri }, activation_args: ActivationArgs { ephemeral }, @@ -196,6 +300,34 @@ fn go(args: Args) -> Result<()> { } } +/// Create and write all bytes from a buffer into a new config file if it doesn't already exist. +fn init_config_file(filepath: &PathBuf, buf: &[u8]) -> Result<()> { + match OpenOptions::new() + .create_new(true) + .write(true) + .truncate(false) + .open(filepath) + { + Ok(mut file) => { + file.write_all(buf)?; + log::info!("{}B written to '{}'", buf.len(), filepath.display()) + } + Err(err) if matches!(err.kind(), std::io::ErrorKind::AlreadyExists) => { + log::warn!( + "'{}' already exists, leaving it unchanged...", + filepath.display() + ) + } + Err(err) => { + bail!( + "failed to initialize system configuration at '{}': {err:?}", + filepath.display() + ) + } + } + Ok(()) +} + fn print_store_path>(store_path: SP) -> Result<()> { // Print the raw store path to stdout println!("{}", store_path.as_ref()); diff --git a/templates/nixos/flake.nix b/templates/nixos/flake.nix new file mode 100644 index 00000000..4ea2af1d --- /dev/null +++ b/templates/nixos/flake.nix @@ -0,0 +1,33 @@ +{ + description = "NixOS System Manager configuration"; + + inputs = { + # Specify the source of System Manager and Nixpkgs. + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + system-manager = { + url = "github:numtide/system-manager"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + system-manager, + ... + }: + let + system = "x86_64-linux"; + in + { + nixosConfigurations.default = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + ./configuration.nix + ./system.nix + system-manager.nixosModules.system-manager + ]; + }; + }; +} diff --git a/templates/standalone/flake.nix b/templates/standalone/flake.nix new file mode 100644 index 00000000..57ea20e7 --- /dev/null +++ b/templates/standalone/flake.nix @@ -0,0 +1,32 @@ +{ + description = "Standalone System Manager configuration"; + + inputs = { + # Specify the source of System Manager and Nixpkgs. + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + system-manager = { + url = "github:numtide/system-manager"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + system-manager, + ... + }: + let + system = "x86_64-linux"; + in + { + systemConfigs.default = system-manager.lib.makeSystemConfig { + # Specify your system configuration modules here, for example, + # the path to your system.nix. + modules = [ ./system.nix ]; + + # Optionally specify extraSpecialArgs and overlays + }; + }; +} diff --git a/templates/system.nix b/templates/system.nix new file mode 100644 index 00000000..b5ebcf24 --- /dev/null +++ b/templates/system.nix @@ -0,0 +1,55 @@ +{ lib, pkgs, ... }: +{ + config = { + nixpkgs.hostPlatform = "x86_64-linux"; + + # Enable and configure services + services = { + # nginx.enable = true; + }; + + environment = { + # Packages that should be installed on a system + systemPackages = [ + # pkgs.hello + ]; + + # Add directories and files to `/etc` and set their permissions + etc = { + # with_ownership = { + # text = '' + # This is just a test! + # ''; + # mode = "0755"; + # uid = 5; + # gid = 6; + # }; + # + # with_ownership2 = { + # text = '' + # This is just a test! + # ''; + # mode = "0755"; + # user = "nobody"; + # group = "users"; + # }; + }; + }; + + # Enable and configure systemd services + systemd.services = { }; + + # Configure systemd tmpfile settings + systemd.tmpfiles = { + # rules = [ + # "D /var/tmp/system-manager 0755 root root -" + # ]; + # + # settings.sample = { + # "/var/tmp/sample".d = { + # mode = "0755"; + # }; + # }; + }; + }; +}