Implement files under /etc

This commit is contained in:
R-VdP 2023-02-13 20:24:23 +01:00
parent 8b3bba30af
commit b3c7f71456
No known key found for this signature in database
10 changed files with 494 additions and 242 deletions

View file

@ -57,7 +57,7 @@
inherit system; inherit system;
overlays = [ (import rust-overlay) devshell.overlay ]; overlays = [ (import rust-overlay) devshell.overlay ];
}; };
rust = pkgs.rust-bin.stable."1.66.0"; rust = pkgs.rust-bin.stable."1.67.1";
llvm = pkgs.llvmPackages_latest; llvm = pkgs.llvmPackages_latest;
craneLib = (crane.mkLib pkgs).overrideToolchain rust.default; craneLib = (crane.mkLib pkgs).overrideToolchain rust.default;
@ -100,7 +100,7 @@
}; };
in in
{ {
serviceConfig = self.lib.makeServiceConfig { systemConfig = self.lib.makeServiceConfig {
inherit system; inherit system;
modules = [ modules = [
./nix/modules ./nix/modules

View file

@ -12,10 +12,19 @@ in
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
nixosConfig = lib.nixosSystem { nixosConfig = (lib.nixosSystem {
inherit system modules; inherit system;
modules = [ ./modules/system-manager.nix ] ++ modules;
specialArgs = { }; specialArgs = { };
}; }).config;
returnIfNoAssertions = drv:
let
failedAssertions = map (x: x.message) (lib.filter (x: !x.assertion) nixosConfig.assertions);
in
if failedAssertions != [ ]
then throw "\nFailed assertions:\n${lib.concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
else lib.showWarnings nixosConfig.warnings drv;
services = services =
lib.listToAttrs lib.listToAttrs
@ -24,21 +33,69 @@ in
let let
serviceName = "${name}.service"; serviceName = "${name}.service";
in in
lib.nameValuePair serviceName { storePath = ''${nixosConfig.config.systemd.units."${serviceName}".unit}/${serviceName}''; }) lib.nameValuePair serviceName {
nixosConfig.config.system-manager.services); storePath =
''${nixosConfig.systemd.units."${serviceName}".unit}/${serviceName}'';
})
nixosConfig.system-manager.services);
servicesPath = pkgs.writeTextFile { servicesPath = pkgs.writeTextFile {
name = "services"; name = "services";
destination = "/services.json"; destination = "/services.json";
text = lib.generators.toJSON { } services; text = lib.generators.toJSON { } services;
}; };
# TODO: handle globbing
etcFiles =
let
isManaged = name: lib.elem name nixosConfig.system-manager.etcFiles;
addToStore = name: file: pkgs.runCommandLocal "${name}-etc-link" { } ''
mkdir -p "$out/etc/$(dirname "${file.target}")"
ln -s "${file.source}" "$out/etc/${file.target}"
if [ "${file.mode}" != symlink ]; then
echo "${file.mode}" > "$out/etc/${file.target}.mode"
echo "${file.user}" > "$out/etc/${file.target}.uid"
echo "${file.group}" > "$out/etc/${file.target}.gid"
fi
'';
filteredEntries = lib.filterAttrs
(name: etcFile: etcFile.enable && isManaged name)
nixosConfig.environment.etc;
srcDrvs = lib.mapAttrs addToStore filteredEntries;
entries = lib.mapAttrs
(name: file: file // { source = "${srcDrvs.${name}}"; })
filteredEntries;
staticEnv = pkgs.buildEnv {
name = "etc-static-env";
paths = lib.attrValues srcDrvs;
};
in
{ inherit entries staticEnv; };
etcPath = pkgs.writeTextFile {
name = "etcFiles";
destination = "/etcFiles.json";
text = lib.generators.toJSON { } etcFiles;
};
# TODO: remove --ephemeral
activationScript = pkgs.writeShellScript "activate" '' activationScript = pkgs.writeShellScript "activate" ''
${system-manager}/bin/system-manager activate \ ${system-manager}/bin/system-manager activate \
--store-path "$(realpath $(dirname ''${0}))" --store-path "$(realpath $(dirname ''${0}))" \
"$@"
''; '';
in in
returnIfNoAssertions (
pkgs.linkFarmFromDrvs "system-manager" [ pkgs.linkFarmFromDrvs "system-manager" [
servicesPath servicesPath
etcPath
activationScript activationScript
]; ]
);
} }

View file

@ -1,5 +1,6 @@
{ lib { lib
, pkgs , pkgs
, config
, ... , ...
}: }:
let let
@ -30,16 +31,42 @@ let
''; '';
}) })
); );
in
{ etcFiles = {
options = { foo = {
system-manager.services = lib.mkOption { text = ''
type = with lib.types; listOf str; This is just a test!
}; '';
target = "foo_test";
}; };
"baz/bar/foo2" = {
text = ''
Another test!
'';
mode = "symlink";
};
foo3 = {
text = "boo!";
mode = "0700";
user = "root";
group = "root";
};
out-of-store = {
source = "/run/systemd/system/";
};
};
in
{
config = { config = {
system-manager.services = lib.attrNames services; system.stateVersion = lib.trivial.release;
system-manager = {
etcFiles = lib.attrNames etcFiles;
services = lib.attrNames services;
};
environment.etc = etcFiles;
systemd = { inherit services; }; systemd = { inherit services; };
}; };
} }

View file

@ -0,0 +1,34 @@
{ lib
, pkgs
, config
, ...
}:
{
options.system-manager = {
services = lib.mkOption {
type = with lib.types; listOf str;
};
etcFiles = lib.mkOption {
type = with lib.types; listOf str;
};
};
config = {
# Avoid some standard NixOS assertions
boot = {
loader.grub.enable = false;
initrd.enable = false;
};
assertions = lib.flip map config.system-manager.etcFiles (entry:
{
assertion = lib.hasAttr entry config.environment.etc;
message = lib.concatStringsSep " " [
"The entry ${entry} that was passed to system-manager.etcFiles"
"is not present in environment.etc"
];
}
);
};
}

View file

@ -1,215 +1,17 @@
mod etc_files;
mod services;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs::DirBuilder;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{fs, io, str};
use super::{ use crate::StorePath;
create_store_link, remove_store_link, systemd, StorePath, STATE_FILE_NAME, SYSTEMD_UNIT_DIR,
SYSTEM_MANAGER_STATE_DIR,
};
#[derive(Debug, Clone, Serialize, Deserialize)] pub fn activate(store_path: StorePath, ephemeral: bool) -> Result<()> {
#[serde(rename_all = "camelCase")]
struct ServiceConfig {
#[serde(flatten)]
store_path: StorePath,
}
type Services = HashMap<String, ServiceConfig>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LinkedServiceConfig {
#[serde(flatten)]
service_config: ServiceConfig,
#[serde(rename = "linkedPath")]
path: String,
}
impl LinkedServiceConfig {
fn linked_path(&self) -> PathBuf {
PathBuf::from(self.path.to_owned())
}
fn new(service_config: ServiceConfig, path: PathBuf) -> Result<Self> {
if let Some(path) = path.to_str() {
return Ok(LinkedServiceConfig {
service_config,
path: String::from(path),
});
}
anyhow::bail!("Could not decode path")
}
}
type LinkedServices = HashMap<String, LinkedServiceConfig>;
pub fn activate(store_path: StorePath) -> Result<()> {
log::info!("Activating system-manager profile: {}", store_path); log::info!("Activating system-manager profile: {}", store_path);
if ephemeral {
log::info!("Running in ephemeral mode");
}
let old_linked_services = read_linked_services()?; etc_files::activate(store_path.clone(), ephemeral)?;
log::debug!("{:?}", old_linked_services); services::activate(store_path, ephemeral)?;
log::info!("Reading service definitions...");
let file = fs::File::open(store_path.store_path + "/services/services.json")?;
let reader = io::BufReader::new(file);
let services: Services = serde_json::from_reader(reader)?;
let linked_services = link_services(services)?;
serialise_linked_services(&linked_services)?;
let services_to_stop = old_linked_services
.into_iter()
.filter(|(name, _)| !linked_services.contains_key(name))
.collect();
let service_manager = systemd::ServiceManager::new_session()?;
let timeout = Some(Duration::from_secs(30));
// We need to do this before we reload the systemd daemon, so that the daemon
// still knows about these units.
stop_services(&service_manager, &services_to_stop, &timeout)?;
unlink_services(&services_to_stop)?;
// We added all new services and removed old ones, so let's reload the units
// to tell systemd about them.
log::info!("Reloading the systemd daemon...");
service_manager.daemon_reload()?;
start_services(&service_manager, &linked_services, &timeout)?;
log::info!("Done");
Ok(())
}
fn unlink_services(services: &LinkedServices) -> Result<()> {
services
.values()
.try_for_each(|linked_service| remove_store_link(linked_service.linked_path().as_path()))
}
fn link_services(services: Services) -> Result<LinkedServices> {
services.iter().try_fold(
HashMap::with_capacity(services.len()),
|mut linked_services, (name, service_config)| {
let linked_path = PathBuf::from(format!("{}/{}", SYSTEMD_UNIT_DIR, name));
match create_store_link(&service_config.store_path, linked_path.as_path()) {
Ok(_) => {
linked_services.insert(
name.to_owned(),
LinkedServiceConfig::new(service_config.to_owned(), linked_path)?,
);
}
e @ Err(_) => {
log::error!("Error linking service {}, skipping.", name);
log::error!("{:?}", e);
}
};
Ok(linked_services)
},
)
}
// FIXME: we should probably lock this file to avoid concurrent writes
fn serialise_linked_services(linked_services: &LinkedServices) -> Result<()> {
let state_file = format!("{}/{}", SYSTEM_MANAGER_STATE_DIR, STATE_FILE_NAME);
DirBuilder::new()
.recursive(true)
.create(SYSTEM_MANAGER_STATE_DIR)?;
log::info!("Writing state info into file: {}", state_file);
let writer = io::BufWriter::new(fs::File::create(state_file)?);
serde_json::to_writer(writer, linked_services)?;
Ok(())
}
fn read_linked_services() -> Result<LinkedServices> {
let state_file = format!("{}/{}", SYSTEM_MANAGER_STATE_DIR, STATE_FILE_NAME);
DirBuilder::new()
.recursive(true)
.create(SYSTEM_MANAGER_STATE_DIR)?;
if Path::new(&state_file).is_file() {
log::info!("Reading state info from {}", state_file);
let reader = io::BufReader::new(fs::File::open(state_file)?);
match serde_json::from_reader(reader) {
Ok(linked_services) => return Ok(linked_services),
Err(e) => {
log::error!("Error reading the state file, ignoring.");
log::error!("{:?}", e);
}
}
}
Ok(HashMap::default())
}
fn start_services(
service_manager: &systemd::ServiceManager,
services: &LinkedServices,
timeout: &Option<Duration>,
) -> Result<()> {
for_each_service(
|s| service_manager.start_unit(s),
service_manager,
services,
timeout,
"restarting",
)
}
fn stop_services(
service_manager: &systemd::ServiceManager,
services: &LinkedServices,
timeout: &Option<Duration>,
) -> Result<()> {
for_each_service(
|s| service_manager.stop_unit(s),
service_manager,
services,
timeout,
"stopping",
)
}
fn for_each_service<F, R>(
action: F,
service_manager: &systemd::ServiceManager,
services: &LinkedServices,
timeout: &Option<Duration>,
log_action: &str,
) -> Result<()>
where
F: Fn(&str) -> Result<R>,
{
let job_monitor = service_manager.monitor_jobs_init()?;
let successful_services = services.keys().fold(
HashSet::with_capacity(services.len()),
|mut set, service| match action(service) {
Ok(_) => {
log::info!("Service {}: {}...", service, log_action);
set.insert(Box::new(service.to_owned()));
set
}
Err(e) => {
log::error!(
"Service {}: error {}, please consult the logs",
service,
log_action
);
log::error!("{}", e);
set
}
},
);
if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? {
anyhow::bail!("Timeout waiting for systemd jobs");
}
// TODO: do we want to propagate unit failures here in some way?
Ok(()) Ok(())
} }

83
src/activate/etc_files.rs Normal file
View file

@ -0,0 +1,83 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::DirBuilder;
use std::path;
use std::path::{Path, PathBuf};
use std::{fs, io};
use crate::{create_link, create_store_link, StorePath};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EtcFile {
source: StorePath,
target: PathBuf,
uid: u32,
gid: u32,
group: String,
user: String,
mode: String,
}
type EtcFiles = HashMap<String, EtcFile>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EtcFilesConfig {
entries: EtcFiles,
static_env: StorePath,
}
pub fn activate(store_path: StorePath, ephemeral: bool) -> Result<()> {
log::info!("Reading etc file definitions...");
let file = fs::File::open(Path::new(&store_path.store_path).join("etcFiles/etcFiles.json"))?;
let reader = io::BufReader::new(file);
let config: EtcFilesConfig = serde_json::from_reader(reader)?;
log::debug!("{:?}", config);
let etc_dir = etc_dir(ephemeral);
log::debug!("Storing /etc entries in {}", etc_dir.display());
DirBuilder::new().recursive(true).create(&etc_dir)?;
create_store_link(
&config.static_env,
etc_dir.join(".system-manager-static").as_path(),
)?;
config
.entries
.into_iter()
.try_for_each(|(name, entry)| create_etc_link(&name, &entry, &etc_dir))?;
Ok(())
}
fn create_etc_link(name: &str, entry: &EtcFile, etc_dir: &Path) -> Result<()> {
if entry.mode == "symlink" {
if let Some(path::Component::Normal(link_target)) =
entry.target.components().into_iter().next()
{
create_link(
Path::new(".")
.join(".system-manager-static")
.join("etc")
.join(link_target)
.as_path(),
etc_dir.join(link_target).as_path(),
)
} else {
anyhow::bail!("Cannot create link for this entry ({}).", name)
}
} else {
Ok(())
}
}
fn etc_dir(ephemeral: bool) -> PathBuf {
if ephemeral {
Path::new("/run").join("etc")
} else {
Path::new("/etc").to_path_buf()
}
}

224
src/activate/services.rs Normal file
View file

@ -0,0 +1,224 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs::DirBuilder;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{fs, io, str};
use crate::{
create_store_link, remove_link, systemd, StorePath, SERVICES_STATE_FILE_NAME,
SYSTEM_MANAGER_STATE_DIR,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceConfig {
store_path: StorePath,
}
type Services = HashMap<String, ServiceConfig>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LinkedServiceConfig {
#[serde(flatten)]
service_config: ServiceConfig,
#[serde(rename = "linkedPath")]
path: PathBuf,
}
impl LinkedServiceConfig {
fn linked_path(&self) -> PathBuf {
PathBuf::from(&self.path)
}
fn new(service_config: ServiceConfig, path: PathBuf) -> Self {
LinkedServiceConfig {
service_config,
path,
}
}
}
type LinkedServices = HashMap<String, LinkedServiceConfig>;
pub fn activate(store_path: StorePath, ephemeral: bool) -> Result<()> {
let old_linked_services = read_linked_services()?;
log::debug!("{:?}", old_linked_services);
log::info!("Reading service definitions...");
let file = fs::File::open(
Path::new(&store_path.store_path)
.join("services")
.join("services.json"),
)?;
let reader = io::BufReader::new(file);
let services: Services = serde_json::from_reader(reader)?;
let linked_services = link_services(services, ephemeral)?;
serialise_linked_services(&linked_services)?;
let services_to_stop = old_linked_services
.into_iter()
.filter(|(name, _)| !linked_services.contains_key(name))
.collect();
let service_manager = systemd::ServiceManager::new_session()?;
let timeout = Some(Duration::from_secs(30));
// We need to do this before we reload the systemd daemon, so that the daemon
// still knows about these units.
stop_services(&service_manager, &services_to_stop, &timeout)?;
unlink_services(&services_to_stop)?;
// We added all new services and removed old ones, so let's reload the units
// to tell systemd about them.
log::info!("Reloading the systemd daemon...");
service_manager.daemon_reload()?;
start_services(&service_manager, &linked_services, &timeout)?;
log::info!("Done");
Ok(())
}
fn unlink_services(services: &LinkedServices) -> Result<()> {
services
.values()
.try_for_each(|linked_service| remove_link(linked_service.linked_path().as_path()))
}
fn link_services(services: Services, ephemeral: bool) -> Result<LinkedServices> {
let systemd_system_dir = systemd_system_dir(ephemeral);
services.iter().try_fold(
HashMap::with_capacity(services.len()),
|mut linked_services, (name, service_config)| {
let linked_path = systemd_system_dir.join(name);
match create_store_link(&service_config.store_path, linked_path.as_path()) {
Ok(_) => {
linked_services.insert(
name.to_owned(),
LinkedServiceConfig::new(service_config.to_owned(), linked_path),
);
}
Err(e) => {
log::error!("Error linking service {}, skipping.", name);
log::error!("{:?}", e);
}
};
Ok(linked_services)
},
)
}
fn systemd_system_dir(ephemeral: bool) -> PathBuf {
(if ephemeral {
Path::new("/run")
} else {
Path::new("/etc")
})
.join("systemd")
.join("system")
}
// FIXME: we should probably lock this file to avoid concurrent writes
fn serialise_linked_services(linked_services: &LinkedServices) -> Result<()> {
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(SERVICES_STATE_FILE_NAME);
DirBuilder::new()
.recursive(true)
.create(SYSTEM_MANAGER_STATE_DIR)?;
log::info!("Writing state info into file: {}", state_file.display());
let writer = io::BufWriter::new(fs::File::create(state_file)?);
serde_json::to_writer(writer, linked_services)?;
Ok(())
}
fn read_linked_services() -> Result<LinkedServices> {
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(SERVICES_STATE_FILE_NAME);
DirBuilder::new()
.recursive(true)
.create(SYSTEM_MANAGER_STATE_DIR)?;
if Path::new(&state_file).is_file() {
log::info!("Reading state info from {}", state_file.display());
let reader = io::BufReader::new(fs::File::open(state_file)?);
match serde_json::from_reader(reader) {
Ok(linked_services) => return Ok(linked_services),
Err(e) => {
log::error!("Error reading the state file, ignoring.");
log::error!("{:?}", e);
}
}
}
Ok(HashMap::default())
}
fn start_services(
service_manager: &systemd::ServiceManager,
services: &LinkedServices,
timeout: &Option<Duration>,
) -> Result<()> {
for_each_service(
|s| service_manager.start_unit(s),
service_manager,
services,
timeout,
"restarting",
)
}
fn stop_services(
service_manager: &systemd::ServiceManager,
services: &LinkedServices,
timeout: &Option<Duration>,
) -> Result<()> {
for_each_service(
|s| service_manager.stop_unit(s),
service_manager,
services,
timeout,
"stopping",
)
}
fn for_each_service<F, R>(
action: F,
service_manager: &systemd::ServiceManager,
services: &LinkedServices,
timeout: &Option<Duration>,
log_action: &str,
) -> Result<()>
where
F: Fn(&str) -> Result<R>,
{
let job_monitor = service_manager.monitor_jobs_init()?;
let successful_services = services.keys().fold(
HashSet::with_capacity(services.len()),
|mut set, service| match action(service) {
Ok(_) => {
log::info!("Service {}: {}...", service, log_action);
set.insert(Box::new(service.to_owned()));
set
}
Err(e) => {
log::error!(
"Service {}: error {}, please consult the logs",
service,
log_action
);
log::error!("{}", e);
set
}
},
);
if !service_manager.monitor_jobs_finish(job_monitor, timeout, successful_services)? {
anyhow::bail!("Timeout waiting for systemd jobs");
}
// TODO: do we want to propagate unit failures here in some way?
Ok(())
}

View file

@ -15,7 +15,7 @@ struct NixBuildOutput {
pub fn generate(flake_uri: &str) -> Result<()> { pub fn generate(flake_uri: &str) -> Result<()> {
// FIXME: we should not hard-code the system here // FIXME: we should not hard-code the system here
let flake_attr = format!("{}.x86_64-linux", FLAKE_ATTR); let flake_attr = format!("{FLAKE_ATTR}.x86_64-linux");
log::info!("Building new system-manager generation..."); log::info!("Building new system-manager generation...");
log::info!("Running nix build..."); log::info!("Running nix build...");
@ -83,7 +83,7 @@ fn parse_nix_build_output(output: String) -> Result<StorePath> {
fn run_nix_build(flake_uri: &str, flake_attr: &str) -> Result<process::Output> { fn run_nix_build(flake_uri: &str, flake_attr: &str) -> Result<process::Output> {
process::Command::new("nix") process::Command::new("nix")
.arg("build") .arg("build")
.arg(format!("{}#{}", flake_uri, flake_attr)) .arg(format!("{flake_uri}#{flake_attr}"))
.arg("--json") .arg("--json")
.output() .output()
.map_err(anyhow::Error::from) .map_err(anyhow::Error::from)

View file

@ -5,45 +5,65 @@ mod systemd;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::os::unix; use std::os::unix;
use std::path::Path; use std::path::{Path, PathBuf};
use std::{fs, str}; use std::{fs, str};
const FLAKE_ATTR: &str = "serviceConfig"; const FLAKE_ATTR: &str = "systemConfig";
const PROFILE_PATH: &str = "/nix/var/nix/profiles/system-manager"; const PROFILE_PATH: &str = "/nix/var/nix/profiles/system-manager";
const GCROOT_PATH: &str = "/nix/var/nix/gcroots/system-manager-current"; const GCROOT_PATH: &str = "/nix/var/nix/gcroots/system-manager-current";
const SYSTEMD_UNIT_DIR: &str = "/run/systemd/system";
const SYSTEM_MANAGER_STATE_DIR: &str = "/var/lib/system-manager/state"; const SYSTEM_MANAGER_STATE_DIR: &str = "/var/lib/system-manager/state";
const STATE_FILE_NAME: &str = "services.json"; const SERVICES_STATE_FILE_NAME: &str = "services.json";
//const ETC_STATE_FILE_NAME: &str = "etc-files.json";
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(from = "String", into = "String", rename_all = "camelCase")]
pub struct StorePath { pub struct StorePath {
pub store_path: String, pub store_path: PathBuf,
} }
impl From<String> for StorePath { impl From<String> for StorePath {
fn from(path: String) -> Self { fn from(path: String) -> Self {
StorePath { // FIXME: handle this better
store_path: path.trim().into(), if !path.starts_with("/nix/store/") {
panic!("Error constructing store path, not in store: {path}");
} }
StorePath {
store_path: PathBuf::from(path),
}
}
}
impl From<StorePath> for PathBuf {
fn from(value: StorePath) -> Self {
value.store_path
}
}
impl From<StorePath> for String {
fn from(value: StorePath) -> Self {
format!("{}", value.store_path.display())
} }
} }
impl std::fmt::Display for StorePath { impl std::fmt::Display for StorePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.store_path) write!(f, "{}", self.store_path.display())
} }
} }
fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> { fn create_store_link(store_path: &StorePath, from: &Path) -> Result<()> {
log::info!("Creating symlink: {} -> {}", from.display(), store_path); create_link(Path::new(&store_path.store_path), from)
}
fn create_link(to: &Path, from: &Path) -> Result<()> {
log::info!("Creating symlink: {} -> {}", from.display(), to.display());
if from.is_symlink() { if from.is_symlink() {
fs::remove_file(from)?; fs::remove_file(from)?;
} }
unix::fs::symlink(&store_path.store_path, from).map_err(anyhow::Error::from) unix::fs::symlink(to, from).map_err(anyhow::Error::from)
} }
fn remove_store_link(from: &Path) -> Result<()> { fn remove_link(from: &Path) -> Result<()> {
log::info!("Removing symlink: {}", from.display()); log::info!("Removing symlink: {}", from.display());
if from.is_symlink() { if from.is_symlink() {
fs::remove_file(from)?; fs::remove_file(from)?;

View file

@ -17,6 +17,8 @@ enum Action {
Activate { Activate {
#[arg(long)] #[arg(long)]
store_path: StorePath, store_path: StorePath,
#[arg(long, action)]
ephemeral: bool,
}, },
Generate { Generate {
#[arg(long)] #[arg(long)]
@ -35,7 +37,10 @@ fn main() -> ExitCode {
fn go(action: Action) -> Result<()> { fn go(action: Action) -> Result<()> {
check_root()?; check_root()?;
match action { match action {
Action::Activate { store_path } => system_manager::activate::activate(store_path), Action::Activate {
store_path,
ephemeral,
} => system_manager::activate::activate(store_path, ephemeral),
Action::Generate { flake_uri } => system_manager::generate::generate(&flake_uri), Action::Generate { flake_uri } => system_manager::generate::generate(&flake_uri),
} }
} }
@ -48,7 +53,7 @@ fn check_root() -> Result<()> {
} }
fn handle_toplevel_error<T>(r: Result<T>) -> ExitCode { fn handle_toplevel_error<T>(r: Result<T>) -> ExitCode {
if let Err(e) = &r { if let Err(e) = r {
log::error!("{}", e); log::error!("{}", e);
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }