Curing existing /nix (#310)

* Curing existing /nix

* Fixup macs

* Suggest an uninstall command if the binary is not present

* Fixup some nits

* Skip a not great suggestion

* Suggest a nice url
This commit is contained in:
Ana Hobden 2023-03-08 10:43:57 -08:00 committed by GitHub
parent b7d7afd02e
commit 85abfc3cb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 246 additions and 119 deletions

View file

@ -78,7 +78,7 @@ impl CreateDirectory {
}
tracing::debug!("Creating directory `{}` already complete", path.display(),);
ActionState::Skipped
ActionState::Completed
} else {
ActionState::Uncompleted
};

View file

@ -9,6 +9,7 @@ pub(crate) mod create_or_merge_nix_config;
pub(crate) mod create_user;
pub(crate) mod fetch_and_unpack_nix;
pub(crate) mod move_unpacked_nix;
pub(crate) mod remove_directory;
pub(crate) mod setup_default_profile;
pub use add_user_to_group::AddUserToGroup;
@ -20,4 +21,5 @@ pub use create_or_merge_nix_config::CreateOrMergeNixConfig;
pub use create_user::CreateUser;
pub use fetch_and_unpack_nix::{FetchAndUnpackNix, FetchUrlError};
pub use move_unpacked_nix::{MoveUnpackedNix, MoveUnpackedNixError};
pub use remove_directory::RemoveDirectory;
pub use setup_default_profile::{SetupDefaultProfile, SetupDefaultProfileError};

View file

@ -11,14 +11,14 @@ Move an unpacked Nix at `src` to `/nix`
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct MoveUnpackedNix {
src: PathBuf,
unpacked_path: PathBuf,
}
impl MoveUnpackedNix {
#[tracing::instrument(level = "debug", skip_all)]
pub async fn plan(src: PathBuf) -> Result<StatefulAction<Self>, ActionError> {
pub async fn plan(unpacked_path: PathBuf) -> Result<StatefulAction<Self>, ActionError> {
// Note: Do NOT try to check for the src/dest since the installer creates those
Ok(Self { src }.into())
Ok(Self { unpacked_path }.into())
}
}
@ -36,7 +36,7 @@ impl Action for MoveUnpackedNix {
span!(
tracing::Level::DEBUG,
"mount_unpacked_nix",
src = tracing::field::display(self.src.display()),
src = tracing::field::display(self.unpacked_path.display()),
dest = DEST,
)
}
@ -46,44 +46,56 @@ impl Action for MoveUnpackedNix {
format!("Move the downloaded Nix into `/nix`"),
vec![format!(
"Nix is being downloaded to `{}` and should be in `/nix`",
self.src.display(),
self.unpacked_path.display(),
)],
)]
}
#[tracing::instrument(level = "debug", skip_all)]
async fn execute(&mut self) -> Result<(), ActionError> {
let Self { src } = self;
let Self { unpacked_path } = self;
// TODO(@Hoverbear): I would like to make this less awful
let found_nix_paths = glob::glob(&format!("{}/nix-*", src.display()))
// This is the `nix-$VERSION` folder which unpacks from the tarball, not a nix derivation
let found_nix_paths = glob::glob(&format!("{}/nix-*", unpacked_path.display()))
.map_err(|e| ActionError::Custom(Box::new(e)))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| ActionError::Custom(Box::new(e)))?;
assert_eq!(
found_nix_paths.len(),
1,
"Did not expect to find multiple nix paths, please report this"
);
if found_nix_paths.len() != 1 {
return Err(ActionError::MalformedBinaryTarball);
}
let found_nix_path = found_nix_paths.into_iter().next().unwrap();
let src_store = found_nix_path.join("store");
let dest = Path::new(DEST).join("store");
tracing::trace!(src = %src_store.display(), dest = %dest.display(), "Renaming");
tokio::fs::rename(&src_store, &dest)
let mut src_store_listing = tokio::fs::read_dir(src_store.clone())
.await
.map_err(|e| ActionError::Rename(src_store.clone(), dest.to_owned(), e))?;
let src_reginfo = found_nix_path.join(".reginfo");
// Move_unpacked_nix expects it here
let dest_reginfo = Path::new(DEST).join(".reginfo");
tokio::fs::rename(&src_reginfo, &dest_reginfo)
.map_err(|e| ActionError::ReadDir(src_store.clone(), e))?;
let dest_store = Path::new(DEST).join("store");
if dest_store.exists() {
if !dest_store.is_dir() {
return Err(ActionError::PathWasNotDirectory(dest_store.clone()))?;
}
} else {
tokio::fs::create_dir(&dest_store)
.await
.map_err(|e| ActionError::Rename(src_reginfo.clone(), dest_reginfo.to_owned(), e))?;
.map_err(|e| ActionError::CreateDirectory(dest_store.clone(), e))?;
}
tokio::fs::remove_dir_all(&src)
while let Some(entry) = src_store_listing
.next_entry()
.await
.map_err(|e| ActionError::Remove(src.clone(), e))?;
.map_err(|e| ActionError::ReadDir(src_store.clone(), e))?
{
let entry_dest = dest_store.join(entry.file_name());
if entry_dest.exists() {
tracing::trace!(src = %entry.path().display(), dest = %entry_dest.display(), "Skipping, already exists");
} else {
tracing::trace!(src = %entry.path().display(), dest = %entry_dest.display(), "Renaming");
tokio::fs::rename(&entry.path(), &entry_dest)
.await
.map_err(|e| {
ActionError::Rename(entry.path().clone(), entry_dest.to_owned(), e)
})?;
}
}
Ok(())
}

View file

@ -0,0 +1,76 @@
use std::path::{Path, PathBuf};
use tokio::fs::remove_dir_all;
use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionState};
use crate::action::{ActionError, StatefulAction};
/** Remove a directory, does nothing on revert.
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct RemoveDirectory {
path: PathBuf,
}
impl RemoveDirectory {
#[tracing::instrument(level = "debug", skip_all)]
pub async fn plan(path: impl AsRef<Path>) -> Result<StatefulAction<Self>, ActionError> {
let path = path.as_ref().to_path_buf();
Ok(StatefulAction {
action: Self {
path: path.to_path_buf(),
},
state: ActionState::Uncompleted,
})
}
}
#[async_trait::async_trait]
#[typetag::serde(name = "remove_directory")]
impl Action for RemoveDirectory {
fn action_tag() -> crate::action::ActionTag {
crate::action::ActionTag("remove_directory")
}
fn tracing_synopsis(&self) -> String {
format!("Remove directory `{}`", self.path.display())
}
fn tracing_span(&self) -> Span {
span!(
tracing::Level::DEBUG,
"remove_directory",
path = tracing::field::display(self.path.display()),
)
}
fn execute_description(&self) -> Vec<ActionDescription> {
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
}
#[tracing::instrument(level = "debug", skip_all)]
async fn execute(&mut self) -> Result<(), ActionError> {
if self.path.exists() {
if !self.path.is_dir() {
return Err(ActionError::PathWasNotDirectory(self.path.clone()));
}
remove_dir_all(&self.path)
.await
.map_err(|e| ActionError::Remove(self.path.clone(), e))?;
} else {
tracing::debug!("Directory `{}` not present, skipping", self.path.display(),);
};
Ok(())
}
fn revert_description(&self) -> Vec<ActionDescription> {
vec![]
}
#[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> {
Ok(())
}
}

View file

@ -1,4 +1,4 @@
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use crate::{
action::{ActionError, ActionTag, StatefulAction},
@ -16,12 +16,14 @@ use crate::action::{Action, ActionDescription};
Setup the default Nix profile with `nss-cacert` and `nix` itself.
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct SetupDefaultProfile {}
pub struct SetupDefaultProfile {
unpacked_path: PathBuf,
}
impl SetupDefaultProfile {
#[tracing::instrument(level = "debug", skip_all)]
pub async fn plan() -> Result<StatefulAction<Self>, ActionError> {
Ok(Self {}.into())
pub async fn plan(unpacked_path: PathBuf) -> Result<StatefulAction<Self>, ActionError> {
Ok(Self { unpacked_path }.into())
}
}
@ -91,9 +93,15 @@ impl Action for SetupDefaultProfile {
)));
};
{
let reginfo_path =
Path::new(crate::action::base::move_unpacked_nix::DEST).join(".reginfo");
let found_nix_paths = glob::glob(&format!("{}/nix-*", self.unpacked_path.display()))
.map_err(|e| ActionError::Custom(Box::new(e)))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| ActionError::Custom(Box::new(e)))?;
if found_nix_paths.len() != 1 {
return Err(ActionError::MalformedBinaryTarball);
}
let found_nix_path = found_nix_paths.into_iter().next().unwrap();
let reginfo_path = PathBuf::from(found_nix_path).join(".reginfo");
let reginfo = tokio::fs::read(&reginfo_path)
.await
.map_err(|e| ActionError::Read(reginfo_path.to_path_buf(), e))?;
@ -140,7 +148,6 @@ impl Action for SetupDefaultProfile {
if !output.status.success() {
return Err(ActionError::command_output(&load_db_command, output));
};
}
// Install `nix` itself into the store
execute_command(

View file

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use tokio::process::Command;
use tracing::{span, Span};
@ -138,6 +138,7 @@ impl Action for ConfigureInitService {
#[cfg(target_os = "linux")]
InitSystem::Systemd => {
tracing::trace!(src = TMPFILES_SRC, dest = TMPFILES_DEST, "Symlinking");
if !Path::new(TMPFILES_DEST).exists() {
tokio::fs::symlink(TMPFILES_SRC, TMPFILES_DEST)
.await
.map_err(|e| {
@ -147,6 +148,7 @@ impl Action for ConfigureInitService {
e,
)
})?;
}
execute_command(
Command::new("systemd-tmpfiles")

View file

@ -1,10 +1,12 @@
use std::path::PathBuf;
use crate::{
action::{
base::SetupDefaultProfile,
common::{ConfigureShellProfile, PlaceNixConfiguration},
Action, ActionDescription, ActionError, ActionTag, StatefulAction,
},
settings::CommonSettings,
settings::{CommonSettings, SCRATCH_DIR},
};
use tracing::{span, Instrument, Span};
@ -22,7 +24,7 @@ pub struct ConfigureNix {
impl ConfigureNix {
#[tracing::instrument(level = "debug", skip_all)]
pub async fn plan(settings: &CommonSettings) -> Result<StatefulAction<Self>, ActionError> {
let setup_default_profile = SetupDefaultProfile::plan()
let setup_default_profile = SetupDefaultProfile::plan(PathBuf::from(SCRATCH_DIR))
.await
.map_err(|e| ActionError::Child(SetupDefaultProfile::action_tag(), Box::new(e)))?;

View file

@ -6,7 +6,7 @@ use crate::{
base::{FetchAndUnpackNix, MoveUnpackedNix},
Action, ActionDescription, ActionError, ActionTag, StatefulAction,
},
settings::CommonSettings,
settings::{CommonSettings, SCRATCH_DIR},
};
use std::path::PathBuf;
@ -24,10 +24,8 @@ pub struct ProvisionNix {
impl ProvisionNix {
#[tracing::instrument(level = "debug", skip_all)]
pub async fn plan(settings: &CommonSettings) -> Result<StatefulAction<Self>, ActionError> {
let fetch_nix = FetchAndUnpackNix::plan(
settings.nix_package_url.clone(),
PathBuf::from("/nix/temp-install-dir"),
)
let fetch_nix =
FetchAndUnpackNix::plan(settings.nix_package_url.clone(), PathBuf::from(SCRATCH_DIR))
.await?;
let create_users_and_group = CreateUsersAndGroups::plan(settings.clone())
.await
@ -35,7 +33,7 @@ impl ProvisionNix {
let create_nix_tree = CreateNixTree::plan()
.await
.map_err(|e| ActionError::Child(CreateNixTree::action_tag(), Box::new(e)))?;
let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from("/nix/temp-install-dir"))
let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from(SCRATCH_DIR))
.await
.map_err(|e| ActionError::Child(MoveUnpackedNix::action_tag(), Box::new(e)))?;
Ok(Self {

View file

@ -311,8 +311,10 @@ pub enum ActionError {
existing_mode = .1 & 0o777,
planned_mode = .2 & 0o777)]
PathModeMismatch(std::path::PathBuf, u32, u32),
#[error("`{0}` was not a file")]
#[error("Path `{0}` exists, but is not a file, consider removing it with `rm {0}`")]
PathWasNotFile(std::path::PathBuf),
#[error("Path `{0}` exists, but is not a directory, consider removing it with `rm {0}`")]
PathWasNotDirectory(std::path::PathBuf),
#[error("Getting metadata for {0}`")]
GettingMetadata(std::path::PathBuf, #[source] std::io::Error),
#[error("Creating directory `{0}`")]
@ -341,6 +343,8 @@ pub enum ActionError {
),
#[error("Read path `{0}`")]
Read(std::path::PathBuf, #[source] std::io::Error),
#[error("Reading directory `{0}`")]
ReadDir(std::path::PathBuf, #[source] std::io::Error),
#[error("Open path `{0}`")]
Open(std::path::PathBuf, #[source] std::io::Error),
#[error("Write path `{0}`")]
@ -410,6 +414,8 @@ pub enum ActionError {
/// A MacOS (Darwin) plist related error
#[error(transparent)]
Plist(#[from] plist::Error),
#[error("Unexpected binary tarball contents found, the build result from `https://releases.nixos.org/?prefix=nix/` or `nix build nix#hydraJobs.binaryTarball.$SYSTEM` is expected")]
MalformedBinaryTarball,
}
impl ActionError {

View file

@ -71,6 +71,7 @@ impl CommandExecute for Install {
let existing_receipt: Option<InstallPlan> = match Path::new(RECEIPT_LOCATION).exists() {
true => {
tracing::trace!("Reading existing receipt");
let install_plan_string = tokio::fs::read_to_string(&RECEIPT_LOCATION)
.await
.wrap_err("Reading plan")?;
@ -79,6 +80,11 @@ impl CommandExecute for Install {
false => None,
};
let uninstall_command = match Path::new("/nix/nix-installer").exists() {
true => "/nix/nix-installer uninstall".into(),
false => format!("curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix/tag/v{} | sh -s -- uninstall", env!("CARGO_PKG_VERSION")),
};
let mut install_plan = match (planner, plan) {
(Some(planner), None) => {
let chosen_planner: Box<dyn Planner> = planner.clone().boxed();
@ -86,18 +92,15 @@ impl CommandExecute for Install {
match existing_receipt {
Some(existing_receipt) => {
if existing_receipt.planner.typetag_name() != chosen_planner.typetag_name() {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used a different planner, try uninstalling the existing install").red());
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used a different planner, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE)
}
if existing_receipt.planner.settings().map_err(|e| eyre!(e))? != chosen_planner.settings().map_err(|e| eyre!(e))? {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install").red());
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE)
}
if existing_receipt.actions.iter().all(|v| v.state == ActionState::Completed) {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed, try uninstalling and reinstalling if Nix isn't working").red());
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed, try uninstalling (`{uninstall_command}`) and reinstalling if Nix isn't working").red());
return Ok(ExitCode::FAILURE)
}
existing_receipt
} ,
None => {
planner.plan().await.map_err(|e| eyre!(e))?
@ -114,18 +117,19 @@ impl CommandExecute for Install {
let builtin_planner = BuiltinPlanner::from_common_settings(settings)
.await
.map_err(|e| eyre::eyre!(e))?;
match existing_receipt {
Some(existing_receipt) => {
if existing_receipt.planner.typetag_name() != builtin_planner.typetag_name() {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used a different planner, try uninstalling the existing install").red());
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used a different planner, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE)
}
if existing_receipt.planner.settings().map_err(|e| eyre!(e))? != builtin_planner.settings().map_err(|e| eyre!(e))? {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install").red());
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE)
}
if existing_receipt.actions.iter().all(|v| v.state == ActionState::Completed) {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed, try uninstalling and reinstalling if Nix isn't working").red());
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed, try uninstalling (`{uninstall_command}`) and reinstalling if Nix isn't working").red());
return Ok(ExitCode::FAILURE)
}
existing_receipt

View file

@ -1,6 +1,6 @@
use crate::{
action::{
base::CreateDirectory,
base::{CreateDirectory, RemoveDirectory},
common::{ConfigureInitService, ConfigureNix, ProvisionNix},
StatefulAction,
},
@ -88,6 +88,10 @@ impl Planner for Linux {
.await
.map_err(PlannerError::Action)?
.boxed(),
RemoveDirectory::plan(crate::settings::SCRATCH_DIR)
.await
.map_err(PlannerError::Action)?
.boxed(),
])
}

View file

@ -6,6 +6,7 @@ use tokio::process::Command;
use crate::{
action::{
base::RemoveDirectory,
common::{ConfigureInitService, ConfigureNix, ProvisionNix},
macos::CreateNixVolume,
StatefulAction,
@ -145,6 +146,10 @@ impl Planner for Macos {
.await
.map_err(PlannerError::Action)?
.boxed(),
RemoveDirectory::plan(crate::settings::SCRATCH_DIR)
.await
.map_err(PlannerError::Action)?
.boxed(),
])
}

View file

@ -63,7 +63,7 @@ use std::{collections::HashMap, path::PathBuf};
use crate::{
action::{
base::{CreateDirectory, CreateFile},
base::{CreateDirectory, CreateFile, RemoveDirectory},
common::{ConfigureInitService, ConfigureNix, ProvisionNix},
linux::StartSystemdUnit,
Action, StatefulAction,
@ -234,6 +234,10 @@ impl Planner for SteamDeck {
.await
.map_err(PlannerError::Action)?
.boxed(),
RemoveDirectory::plan(crate::settings::SCRATCH_DIR)
.await
.map_err(PlannerError::Action)?
.boxed(),
])
}

View file

@ -6,6 +6,8 @@ use std::collections::HashMap;
use clap::ArgAction;
use url::Url;
pub const SCRATCH_DIR: &str = "/nix/temp-install-dir";
/// Default [`nix_package_url`](CommonSettings::nix_package_url) for Linux x86_64
pub const NIX_X64_64_LINUX_URL: &str =
"https://releases.nixos.org/nix/nix-2.13.3/nix-2.13.3-x86_64-linux.tar.xz";

View file

@ -758,7 +758,7 @@
},
"move_unpacked_nix": {
"action": {
"src": "/nix/temp-install-dir"
"unpacked_path": "/nix/temp-install-dir"
},
"state": "Uncompleted"
}
@ -775,7 +775,8 @@
"nixpkgs",
"https://nixos.org/channels/nixpkgs-unstable"
]
]
],
"unpacked_path": "/nix/temp-install-dir"
},
"state": "Uncompleted"
},

View file

@ -802,7 +802,7 @@
},
"move_unpacked_nix": {
"action": {
"src": "/nix/temp-install-dir"
"unpacked_path": "/nix/temp-install-dir"
},
"state": "Uncompleted"
}
@ -819,7 +819,8 @@
"nixpkgs",
"https://nixos.org/channels/nixpkgs-unstable"
]
]
],
"unpacked_path": "/nix/temp-install-dir"
},
"state": "Uncompleted"
},

View file

@ -823,7 +823,7 @@
},
"move_unpacked_nix": {
"action": {
"src": "/nix/temp-install-dir"
"unpacked_path": "/nix/temp-install-dir"
},
"state": "Uncompleted"
}
@ -840,7 +840,8 @@
"nixpkgs",
"https://nixos.org/channels/nixpkgs-unstable"
]
]
],
"unpacked_path": "/nix/temp-install-dir"
},
"state": "Uncompleted"
},