diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6440715..a9f6c5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -292,7 +292,6 @@ jobs: backtrace: full github-token: ${{ secrets.GITHUB_TOKEN }} planner: steam-deck - extra-args: --persistence /home/runner/.ci-test-nix-home - name: Initial uninstall (without a `nix run` first) run: sudo -E /nix/nix-installer uninstall env: @@ -327,7 +326,6 @@ jobs: backtrace: full github-token: ${{ secrets.GITHUB_TOKEN }} planner: steam-deck - extra-args: --persistence /home/runner/.ci-test-nix-home - name: echo $PATH run: echo $PATH - name: Test `nix` with `$GITHUB_PATH` diff --git a/nix/tests/vm-test/default.nix b/nix/tests/vm-test/default.nix index 47c1199..d806da6 100644 --- a/nix/tests/vm-test/default.nix +++ b/nix/tests/vm-test/default.nix @@ -96,8 +96,12 @@ let exit 1 fi - if [ -d /nix ]; then - echo "/nix exists after uninstall" + if [ -d /nix/store ]; then + echo "/nix/store exists after uninstall" + exit 1 + fi + if [ -d /nix/var ]; then + echo "/nix/var exists after uninstall" exit 1 fi @@ -173,6 +177,17 @@ let uninstall = installCases.install-default.uninstall; uninstallCheck = installCases.install-default.uninstallCheck; }; + install-bind-mounted-nix = { + preinstall = '' + sudo mkdir -p /nix + sudo mkdir -p /bind-mount-for-nix + sudo mount --bind /bind-mount-for-nix /nix + ''; + install = installCases.install-default.install; + check = installCases.install-default.check; + uninstall = installCases.install-default.uninstall; + uninstallCheck = installCases.install-default.uninstallCheck; + }; }; cureSelfCases = { cure-self-linux-working = { diff --git a/src/action/base/create_directory.rs b/src/action/base/create_directory.rs index a16c12c..ddd2208 100644 --- a/src/action/base/create_directory.rs +++ b/src/action/base/create_directory.rs @@ -3,11 +3,13 @@ use std::path::{Path, PathBuf}; use nix::unistd::{chown, Group, User}; -use tokio::fs::{create_dir, remove_dir_all}; +use tokio::fs::{create_dir, remove_dir_all, remove_file}; +use tokio::process::Command; use tracing::{span, Span}; use crate::action::{Action, ActionDescription, ActionErrorKind, ActionState}; use crate::action::{ActionError, StatefulAction}; +use crate::execute_command; /** Create a directory at the given location, optionally with an owning user, group, and mode. @@ -20,6 +22,7 @@ pub struct CreateDirectory { user: Option, group: Option, mode: Option, + is_mountpoint: bool, force_prune_on_revert: bool, } @@ -36,6 +39,7 @@ impl CreateDirectory { let user = user.into(); let group = group.into(); let mode = mode.into(); + let mut is_mountpoint = false; let action_state = if path.exists() { let metadata = tokio::fs::metadata(&path) @@ -84,7 +88,13 @@ impl CreateDirectory { } } - tracing::debug!("Creating directory `{}` already complete", path.display(),); + // Is it a mountpoint? + is_mountpoint = path_is_mountpoint(&path).await.map_err(Self::error)?; + tracing::debug!( + is_mountpoint, + "Creating directory `{}` already complete", + path.display(), + ); ActionState::Completed } else { ActionState::Uncompleted @@ -96,6 +106,7 @@ impl CreateDirectory { user, group, mode, + is_mountpoint, force_prune_on_revert, }, state: action_state, @@ -137,9 +148,15 @@ impl Action for CreateDirectory { user, group, mode, + is_mountpoint, // If `is_mountpoint = true` the `ActionState` should be completed. force_prune_on_revert: _, } = self; + if *is_mountpoint { + // A `/nix` mount exists, we don't need to do anything. + return Ok(()); + } + let gid = if let Some(group) = group { Some( Group::from_name(group.as_str()) @@ -189,20 +206,28 @@ impl Action for CreateDirectory { user: _, group: _, mode: _, + is_mountpoint, force_prune_on_revert, } = &self; - vec![ActionDescription::new( - format!( - "Remove the directory `{}`{}", - path.display(), - if *force_prune_on_revert { - "" - } else { - " if no other contents exists" - } - ), - vec![], - )] + match (is_mountpoint, force_prune_on_revert) { + (true, true) => vec![ActionDescription::new( + format!("Clean contents of mountpoint `{}`", path.display(),), + vec![], + )], + (true, false) => vec![], + (false, _) => vec![ActionDescription::new( + format!( + "Remove the directory `{}`{}", + path.display(), + if *force_prune_on_revert { + "" + } else { + " if no other contents exists" + } + ), + vec![], + )], + } } #[tracing::instrument(level = "debug", skip_all)] @@ -212,22 +237,50 @@ impl Action for CreateDirectory { user: _, group: _, mode: _, + is_mountpoint, force_prune_on_revert, } = self; - let is_empty = path + let contents = path .read_dir() .map_err(|e| ActionErrorKind::Read(path.clone(), e)) .map_err(Self::error)? - .next() - .is_none(); + .collect::>(); + let is_empty = contents.is_empty(); - match (is_empty, force_prune_on_revert) { - (true, _) | (false, true) => remove_dir_all(path.clone()) + match (is_mountpoint, is_empty, force_prune_on_revert) { + (true, _, true) => { + tracing::debug!("Cleaning mountpoint `{}`", path.display()); + for child_path in contents { + let child_path = child_path + .map_err(|e| ActionErrorKind::ReadDir(path.clone(), e)) + .map_err(Self::error)?; + let child_path_path = child_path.path(); + let child_path_type = child_path + .file_type() + .map_err(|e| ActionErrorKind::GettingMetadata(child_path_path.clone(), e)) + .map_err(Self::error)?; + if child_path_type.is_dir() { + remove_dir_all(child_path_path.clone()) + .await + .map_err(|e| ActionErrorKind::Remove(path.clone(), e)) + .map_err(Self::error)? + } else { + remove_file(child_path_path) + .await + .map_err(|e| ActionErrorKind::Remove(path.clone(), e)) + .map_err(Self::error)? + } + } + }, + (true, _, false) => { + tracing::debug!("Not cleaning mountpoint `{}`", path.display()); + }, + (false, true, _) | (false, false, true) => remove_dir_all(path.clone()) .await .map_err(|e| ActionErrorKind::Remove(path.clone(), e)) .map_err(Self::error)?, - (false, false) => { + (false, false, false) => { tracing::debug!("Not removing `{}`, the folder is not empty", path.display()); }, }; @@ -236,6 +289,53 @@ impl Action for CreateDirectory { } } +// There are cleaner ways of doing this (eg `systemctl status $PATH`) however we need a widely supported way. +async fn path_is_mountpoint(path: &Path) -> Result { + let path_str = match path.to_str() { + Some(path_str) => path_str, + None => return Err(ActionErrorKind::PathNoneString(path.to_path_buf())), + }; + + let mut mount_command = Command::new("mount"); + mount_command.process_group(0); + + #[cfg(target_os = "macos")] + mount_command.arg("-d"); // `-d` means `--dry-run` + #[cfg(target_os = "linux")] + mount_command.arg("-f"); // `-f` means `--fake` not `--force` + + let output = execute_command(&mut mount_command).await?; + let output_string = String::from_utf8(output.stdout).map_err(ActionErrorKind::FromUtf8)?; + + for line in output_string.lines() { + let mut line_splitter = line.split(" on "); + match line_splitter.next() { + Some(_device) => (), + None => continue, + } + let destination_and_options = match line_splitter.next() { + Some(destination_and_options) => destination_and_options, + None => continue, + }; + // Each line on Linux looks like `portal on /run/user/1000/doc type fuse.portal (rw,nosuid,nodev,relatime,user_id=1000,group_id=100)` + #[cfg(target_os = "linux")] + let split_token = "type"; + // Each line on MacOS looks like `/dev/disk3s6 on /System/Volumes/VM (apfs, local, noexec, journaled, noatime, nobrowse)` + #[cfg(target_os = "macos")] + let split_token = "("; + + if let Some(mount_path) = destination_and_options.rsplit(split_token).last() { + let trimmed = mount_path.trim(); + if trimmed == path_str { + tracing::trace!("Found mountpoint for `{mount_path}`"); + return Ok(true); + } + } + } + + Ok(false) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/action/common/create_nix_tree.rs b/src/action/common/create_nix_tree.rs index ecc0945..12efb7a 100644 --- a/src/action/common/create_nix_tree.rs +++ b/src/action/common/create_nix_tree.rs @@ -36,7 +36,7 @@ impl CreateNixTree { for path in PATHS { // We use `create_dir` over `create_dir_all` to ensure we always set permissions right create_directories.push( - CreateDirectory::plan(path, String::from("root"), None, 0o0755, false) + CreateDirectory::plan(path, String::from("root"), None, 0o0755, true) .await .map_err(Self::error)?, ) diff --git a/src/action/mod.rs b/src/action/mod.rs index 1eb9c9b..ed9dfd6 100644 --- a/src/action/mod.rs +++ b/src/action/mod.rs @@ -525,6 +525,8 @@ pub enum ActionErrorKind { #[from] std::string::FromUtf8Error, ), + #[error("Path `{}` could not be converted to valid UTF-8 string", .0.display())] + PathNoneString(std::path::PathBuf), /// A MacOS (Darwin) plist related error #[error(transparent)] Plist(#[from] plist::Error), diff --git a/tests/fixtures/linux/linux.json b/tests/fixtures/linux/linux.json index d1a8ceb..681eedf 100644 --- a/tests/fixtures/linux/linux.json +++ b/tests/fixtures/linux/linux.json @@ -8,6 +8,7 @@ "user": null, "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": true }, "state": "Uncompleted" @@ -41,6 +42,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -51,6 +53,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -61,6 +64,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -71,6 +75,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -81,6 +86,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -91,6 +97,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -101,6 +108,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -111,6 +119,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -121,6 +130,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -131,6 +141,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -141,6 +152,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -151,6 +163,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -161,6 +174,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -221,6 +235,7 @@ "user": null, "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Completed" @@ -294,6 +309,7 @@ "user": null, "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" diff --git a/tests/fixtures/linux/steam-deck.json b/tests/fixtures/linux/steam-deck.json index a2456ac..bbd3e60 100644 --- a/tests/fixtures/linux/steam-deck.json +++ b/tests/fixtures/linux/steam-deck.json @@ -8,6 +8,7 @@ "user": null, "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": true }, "state": "Uncompleted" @@ -85,6 +86,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -95,6 +97,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -105,6 +108,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -115,6 +119,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -125,6 +130,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -135,6 +141,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -145,6 +152,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -155,6 +163,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -165,6 +174,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -175,6 +185,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -185,6 +196,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -195,6 +207,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -205,6 +218,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -315,6 +329,7 @@ "user": null, "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" diff --git a/tests/fixtures/macos/macos.json b/tests/fixtures/macos/macos.json index ec47710..ae99895 100644 --- a/tests/fixtures/macos/macos.json +++ b/tests/fixtures/macos/macos.json @@ -110,6 +110,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -120,6 +121,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -130,6 +132,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -140,6 +143,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -150,6 +154,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -160,6 +165,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -170,6 +176,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -180,6 +187,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -190,6 +198,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -200,6 +209,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -210,6 +220,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -220,6 +231,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -230,6 +242,7 @@ "user": "root", "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted" @@ -350,6 +363,7 @@ "user": null, "group": null, "mode": 493, + "is_mountpoint": true, "force_prune_on_revert": false }, "state": "Uncompleted"