Add time machine exclusions for Mac (#480)
* Add time machine exclusions for Mac * Prod CI * Stub out mac test for a remote build * Add plan changes * wip * Move the exclusions to be later * Fixups * Use pathbufs * Import pathbuf * Update test plans
This commit is contained in:
parent
f91b93bdb0
commit
7ec5148e6d
|
@ -10,6 +10,8 @@ pub(crate) mod create_volume_service;
|
|||
pub(crate) mod enable_ownership;
|
||||
pub(crate) mod encrypt_apfs_volume;
|
||||
pub(crate) mod kickstart_launchctl_service;
|
||||
pub(crate) mod set_tmutil_exclusion;
|
||||
pub(crate) mod set_tmutil_exclusions;
|
||||
pub(crate) mod unmount_apfs_volume;
|
||||
|
||||
pub use bootstrap_launchctl_service::BootstrapLaunchctlService;
|
||||
|
@ -21,6 +23,8 @@ pub use enable_ownership::{EnableOwnership, EnableOwnershipError};
|
|||
pub use encrypt_apfs_volume::EncryptApfsVolume;
|
||||
pub use kickstart_launchctl_service::KickstartLaunchctlService;
|
||||
use serde::Deserialize;
|
||||
pub use set_tmutil_exclusion::SetTmutilExclusion;
|
||||
pub use set_tmutil_exclusions::SetTmutilExclusions;
|
||||
use tokio::process::Command;
|
||||
pub use unmount_apfs_volume::UnmountApfsVolume;
|
||||
use uuid::Uuid;
|
||||
|
|
100
src/action/macos/set_tmutil_exclusion.rs
Normal file
100
src/action/macos/set_tmutil_exclusion.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tokio::process::Command;
|
||||
use tracing::{span, Span};
|
||||
|
||||
use crate::action::{ActionError, ActionTag, StatefulAction};
|
||||
use crate::execute_command;
|
||||
|
||||
use crate::action::{Action, ActionDescription};
|
||||
|
||||
/**
|
||||
Set a time machine exclusion on a path.
|
||||
|
||||
Note, this cannot be used on Volumes easily:
|
||||
|
||||
```bash,no_run
|
||||
% sudo tmutil addexclusion -v "Nix Store"
|
||||
tmutil: addexclusion requires Full Disk Access privileges.
|
||||
To allow this operation, select Full Disk Access in the Privacy
|
||||
tab of the Security & Privacy preference pane, and add Terminal
|
||||
to the list of applications which are allowed Full Disk Access.
|
||||
% sudo tmutil addexclusion /nix
|
||||
/nix: The operation couldn’t be completed. Invalid argument
|
||||
```
|
||||
|
||||
*/
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct SetTmutilExclusion {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl SetTmutilExclusion {
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub async fn plan(path: impl AsRef<Path>) -> Result<StatefulAction<Self>, ActionError> {
|
||||
Ok(Self {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[typetag::serde(name = "set_tmutil_exclusion")]
|
||||
impl Action for SetTmutilExclusion {
|
||||
fn action_tag() -> ActionTag {
|
||||
ActionTag("set_tmutil_exclusion")
|
||||
}
|
||||
fn tracing_synopsis(&self) -> String {
|
||||
format!(
|
||||
"Configure Time Machine exclusion on `{}`",
|
||||
self.path.display()
|
||||
)
|
||||
}
|
||||
|
||||
fn tracing_span(&self) -> Span {
|
||||
span!(
|
||||
tracing::Level::DEBUG,
|
||||
"set_tmutil_exclusion",
|
||||
path = %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> {
|
||||
execute_command(
|
||||
Command::new("tmutil")
|
||||
.process_group(0)
|
||||
.arg("addexclusion")
|
||||
.arg(&self.path)
|
||||
.stdin(std::process::Stdio::null()),
|
||||
)
|
||||
.await
|
||||
.map_err(Self::error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn revert_description(&self) -> Vec<ActionDescription> {
|
||||
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn revert(&mut self) -> Result<(), ActionError> {
|
||||
execute_command(
|
||||
Command::new("tmutil")
|
||||
.process_group(0)
|
||||
.arg("removeexclusion")
|
||||
.arg(&self.path)
|
||||
.stdin(std::process::Stdio::null()),
|
||||
)
|
||||
.await
|
||||
.map_err(Self::error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
132
src/action/macos/set_tmutil_exclusions.rs
Normal file
132
src/action/macos/set_tmutil_exclusions.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use tracing::{span, Span};
|
||||
|
||||
use crate::action::{
|
||||
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
|
||||
};
|
||||
|
||||
use super::SetTmutilExclusion;
|
||||
|
||||
/**
|
||||
Set a time machine exclusion on several paths.
|
||||
|
||||
Note, this cannot be used on Volumes easily:
|
||||
|
||||
```bash,no_run
|
||||
% sudo tmutil addexclusion -v "Nix Store"
|
||||
tmutil: addexclusion requires Full Disk Access privileges.
|
||||
To allow this operation, select Full Disk Access in the Privacy
|
||||
tab of the Security & Privacy preference pane, and add Terminal
|
||||
to the list of applications which are allowed Full Disk Access.
|
||||
% sudo tmutil addexclusion /nix
|
||||
/nix: The operation couldn’t be completed. Invalid argument
|
||||
```
|
||||
|
||||
*/
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct SetTmutilExclusions {
|
||||
set_tmutil_exclusions: Vec<StatefulAction<SetTmutilExclusion>>,
|
||||
}
|
||||
|
||||
impl SetTmutilExclusions {
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub async fn plan(paths: Vec<PathBuf>) -> Result<StatefulAction<Self>, ActionError> {
|
||||
/* Testing with `sudo tmutil addexclusion -p /nix` and `sudo tmutil addexclusion -v "Nix Store"` on DetSys's Macs
|
||||
yielded this error:
|
||||
|
||||
```
|
||||
tmutil: addexclusion requires Full Disk Access privileges.
|
||||
To allow this operation, select Full Disk Access in the Privacy
|
||||
tab of the Security & Privacy preference pane, and add Terminal
|
||||
to the list of applications which are allowed Full Disk Access.
|
||||
```
|
||||
|
||||
So we do these subdirectories instead.
|
||||
*/
|
||||
let mut set_tmutil_exclusions = Vec::new();
|
||||
for path in paths {
|
||||
let set_tmutil_exclusion = SetTmutilExclusion::plan(path).await.map_err(Self::error)?;
|
||||
set_tmutil_exclusions.push(set_tmutil_exclusion);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
set_tmutil_exclusions,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[typetag::serde(name = "set_tmutil_exclusions")]
|
||||
impl Action for SetTmutilExclusions {
|
||||
fn action_tag() -> ActionTag {
|
||||
ActionTag("set_tmutil_exclusions")
|
||||
}
|
||||
fn tracing_synopsis(&self) -> String {
|
||||
String::from("Configure Time Machine exclusions")
|
||||
}
|
||||
|
||||
fn tracing_span(&self) -> Span {
|
||||
span!(tracing::Level::DEBUG, "set_tmutil_exclusions",)
|
||||
}
|
||||
|
||||
fn execute_description(&self) -> Vec<ActionDescription> {
|
||||
let Self {
|
||||
set_tmutil_exclusions,
|
||||
} = &self;
|
||||
|
||||
let mut set_tmutil_exclusion_descriptions = Vec::new();
|
||||
for set_tmutil_exclusion in set_tmutil_exclusions {
|
||||
if let Some(val) = set_tmutil_exclusion.describe_execute().iter().next() {
|
||||
set_tmutil_exclusion_descriptions.push(val.description.clone())
|
||||
}
|
||||
}
|
||||
vec![ActionDescription::new(
|
||||
self.tracing_synopsis(),
|
||||
set_tmutil_exclusion_descriptions,
|
||||
)]
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn execute(&mut self) -> Result<(), ActionError> {
|
||||
// Just do sequential since parallelizing this will have little benefit
|
||||
for set_tmutil_exclusion in self.set_tmutil_exclusions.iter_mut() {
|
||||
set_tmutil_exclusion
|
||||
.try_execute()
|
||||
.await
|
||||
.map_err(Self::error)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn revert_description(&self) -> Vec<ActionDescription> {
|
||||
vec![ActionDescription::new(
|
||||
format!("Remove time machine exclusions"),
|
||||
vec![],
|
||||
)]
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn revert(&mut self) -> Result<(), ActionError> {
|
||||
let mut errors = vec![];
|
||||
// Just do sequential since parallelizing this will have little benefit
|
||||
for set_tmutil_exclusion in self.set_tmutil_exclusions.iter_mut().rev() {
|
||||
if let Err(err) = set_tmutil_exclusion.try_revert().await {
|
||||
errors.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else if errors.len() == 1 {
|
||||
Err(errors
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("Expected 1 len Vec to have at least 1 item"))
|
||||
} else {
|
||||
Err(Self::error(ActionErrorKind::MultipleChildren(errors)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use std::{collections::HashMap, io::Cursor};
|
||||
use std::{collections::HashMap, io::Cursor, path::PathBuf};
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
use clap::ArgAction;
|
||||
|
@ -10,7 +10,7 @@ use crate::{
|
|||
action::{
|
||||
base::RemoveDirectory,
|
||||
common::{ConfigureInitService, ConfigureNix, ProvisionNix},
|
||||
macos::CreateNixVolume,
|
||||
macos::{CreateNixVolume, SetTmutilExclusions},
|
||||
StatefulAction,
|
||||
},
|
||||
execute_command,
|
||||
|
@ -130,10 +130,6 @@ impl Planner for Macos {
|
|||
};
|
||||
|
||||
Ok(vec![
|
||||
// Create Volume step:
|
||||
//
|
||||
// setup_Synthetic -> create_synthetic_objects
|
||||
// Unmount -> create_volume -> Setup_fstab -> maybe encrypt_volume -> launchctl bootstrap -> launchctl kickstart -> await_volume -> maybe enableOwnership
|
||||
CreateNixVolume::plan(
|
||||
root_disk.unwrap(), /* We just ensured it was populated */
|
||||
self.volume_label.clone(),
|
||||
|
@ -147,6 +143,10 @@ impl Planner for Macos {
|
|||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
SetTmutilExclusions::plan(vec![PathBuf::from("/nix/store"), PathBuf::from("/nix/var")])
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
.boxed(),
|
||||
ConfigureNix::plan(ShellProfileLocations::default(), &self.settings)
|
||||
.await
|
||||
.map_err(PlannerError::Action)?
|
||||
|
|
|
@ -397,9 +397,9 @@ impl HasExpectedErrors for PlannerError {
|
|||
this @ PlannerError::RosettaDetected => Some(Box::new(this)),
|
||||
PlannerError::Utf8(_) => None,
|
||||
PlannerError::SelinuxRequirements => Some(Box::new(self)),
|
||||
PlannerError::Custom(e) => {
|
||||
PlannerError::Custom(_e) => {
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(err) = e.downcast_ref::<linux::LinuxErrorKind>() {
|
||||
if let Some(err) = _e.downcast_ref::<linux::LinuxErrorKind>() {
|
||||
return err.expected();
|
||||
}
|
||||
None
|
||||
|
|
26
tests/fixtures/macos/macos.json
vendored
26
tests/fixtures/macos/macos.json
vendored
|
@ -93,7 +93,7 @@
|
|||
},
|
||||
"state": "Uncompleted"
|
||||
},
|
||||
"delete_users": [],
|
||||
"delete_users_in_group": null,
|
||||
"create_group": {
|
||||
"action": {
|
||||
"name": "nixbld",
|
||||
|
@ -247,6 +247,26 @@
|
|||
},
|
||||
"state": "Uncompleted"
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"action": "set_tmutil_exclusions",
|
||||
"set_tmutil_exclusions": [
|
||||
{
|
||||
"action": {
|
||||
"path": "/nix/store"
|
||||
},
|
||||
"state": "Uncompleted"
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"path": "/nix/var"
|
||||
},
|
||||
"state": "Uncompleted"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": "Uncompleted"
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"action": "configure_nix",
|
||||
|
@ -339,12 +359,12 @@
|
|||
"path": "/etc/nix/nix.conf",
|
||||
"pending_nix_config": {
|
||||
"settings": {
|
||||
"experimental-features": "nix-command flakes auto-allocate-uids",
|
||||
"extra-nix-path": "nixpkgs=flake:nixpkgs",
|
||||
"auto-allocate-uids": "true",
|
||||
"auto-optimise-store": "true",
|
||||
"build-users-group": "nixbld",
|
||||
"bash-prompt-prefix": "(nix:$name)\\040",
|
||||
"auto-allocate-uids": "true"
|
||||
"experimental-features": "nix-command flakes auto-allocate-uids"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -25,11 +25,11 @@ fn plan_compat_steam_deck() -> eyre::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// Ensure existing plans still parse
|
||||
// If this breaks and you need to update the fixture, disable these tests, bump `nix_installer` to a new version, and update the plans.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn plan_compat_macos() -> eyre::Result<()> {
|
||||
let _: InstallPlan = serde_json::from_str(MACOS)?;
|
||||
Ok(())
|
||||
}
|
||||
// // Ensure existing plans still parse
|
||||
// // If this breaks and you need to update the fixture, disable these tests, bump `nix_installer` to a new version, and update the plans.
|
||||
// #[cfg(target_os = "macos")]
|
||||
// #[test]
|
||||
// fn plan_compat_macos() -> eyre::Result<()> {
|
||||
// let _: InstallPlan = serde_json::from_str(MACOS)?;
|
||||
// Ok(())
|
||||
// }
|
||||
|
|
Loading…
Reference in a new issue