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 enable_ownership;
|
||||||
pub(crate) mod encrypt_apfs_volume;
|
pub(crate) mod encrypt_apfs_volume;
|
||||||
pub(crate) mod kickstart_launchctl_service;
|
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(crate) mod unmount_apfs_volume;
|
||||||
|
|
||||||
pub use bootstrap_launchctl_service::BootstrapLaunchctlService;
|
pub use bootstrap_launchctl_service::BootstrapLaunchctlService;
|
||||||
|
@ -21,6 +23,8 @@ pub use enable_ownership::{EnableOwnership, EnableOwnershipError};
|
||||||
pub use encrypt_apfs_volume::EncryptApfsVolume;
|
pub use encrypt_apfs_volume::EncryptApfsVolume;
|
||||||
pub use kickstart_launchctl_service::KickstartLaunchctlService;
|
pub use kickstart_launchctl_service::KickstartLaunchctlService;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
pub use set_tmutil_exclusion::SetTmutilExclusion;
|
||||||
|
pub use set_tmutil_exclusions::SetTmutilExclusions;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
pub use unmount_apfs_volume::UnmountApfsVolume;
|
pub use unmount_apfs_volume::UnmountApfsVolume;
|
||||||
use uuid::Uuid;
|
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")]
|
#[cfg(feature = "cli")]
|
||||||
use clap::ArgAction;
|
use clap::ArgAction;
|
||||||
|
@ -10,7 +10,7 @@ use crate::{
|
||||||
action::{
|
action::{
|
||||||
base::RemoveDirectory,
|
base::RemoveDirectory,
|
||||||
common::{ConfigureInitService, ConfigureNix, ProvisionNix},
|
common::{ConfigureInitService, ConfigureNix, ProvisionNix},
|
||||||
macos::CreateNixVolume,
|
macos::{CreateNixVolume, SetTmutilExclusions},
|
||||||
StatefulAction,
|
StatefulAction,
|
||||||
},
|
},
|
||||||
execute_command,
|
execute_command,
|
||||||
|
@ -130,10 +130,6 @@ impl Planner for Macos {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(vec![
|
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(
|
CreateNixVolume::plan(
|
||||||
root_disk.unwrap(), /* We just ensured it was populated */
|
root_disk.unwrap(), /* We just ensured it was populated */
|
||||||
self.volume_label.clone(),
|
self.volume_label.clone(),
|
||||||
|
@ -147,6 +143,10 @@ impl Planner for Macos {
|
||||||
.await
|
.await
|
||||||
.map_err(PlannerError::Action)?
|
.map_err(PlannerError::Action)?
|
||||||
.boxed(),
|
.boxed(),
|
||||||
|
SetTmutilExclusions::plan(vec![PathBuf::from("/nix/store"), PathBuf::from("/nix/var")])
|
||||||
|
.await
|
||||||
|
.map_err(PlannerError::Action)?
|
||||||
|
.boxed(),
|
||||||
ConfigureNix::plan(ShellProfileLocations::default(), &self.settings)
|
ConfigureNix::plan(ShellProfileLocations::default(), &self.settings)
|
||||||
.await
|
.await
|
||||||
.map_err(PlannerError::Action)?
|
.map_err(PlannerError::Action)?
|
||||||
|
|
|
@ -397,9 +397,9 @@ impl HasExpectedErrors for PlannerError {
|
||||||
this @ PlannerError::RosettaDetected => Some(Box::new(this)),
|
this @ PlannerError::RosettaDetected => Some(Box::new(this)),
|
||||||
PlannerError::Utf8(_) => None,
|
PlannerError::Utf8(_) => None,
|
||||||
PlannerError::SelinuxRequirements => Some(Box::new(self)),
|
PlannerError::SelinuxRequirements => Some(Box::new(self)),
|
||||||
PlannerError::Custom(e) => {
|
PlannerError::Custom(_e) => {
|
||||||
#[cfg(target_os = "linux")]
|
#[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();
|
return err.expected();
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
26
tests/fixtures/macos/macos.json
vendored
26
tests/fixtures/macos/macos.json
vendored
|
@ -93,7 +93,7 @@
|
||||||
},
|
},
|
||||||
"state": "Uncompleted"
|
"state": "Uncompleted"
|
||||||
},
|
},
|
||||||
"delete_users": [],
|
"delete_users_in_group": null,
|
||||||
"create_group": {
|
"create_group": {
|
||||||
"action": {
|
"action": {
|
||||||
"name": "nixbld",
|
"name": "nixbld",
|
||||||
|
@ -247,6 +247,26 @@
|
||||||
},
|
},
|
||||||
"state": "Uncompleted"
|
"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": {
|
||||||
"action": "configure_nix",
|
"action": "configure_nix",
|
||||||
|
@ -339,12 +359,12 @@
|
||||||
"path": "/etc/nix/nix.conf",
|
"path": "/etc/nix/nix.conf",
|
||||||
"pending_nix_config": {
|
"pending_nix_config": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"experimental-features": "nix-command flakes auto-allocate-uids",
|
|
||||||
"extra-nix-path": "nixpkgs=flake:nixpkgs",
|
"extra-nix-path": "nixpkgs=flake:nixpkgs",
|
||||||
|
"auto-allocate-uids": "true",
|
||||||
"auto-optimise-store": "true",
|
"auto-optimise-store": "true",
|
||||||
"build-users-group": "nixbld",
|
"build-users-group": "nixbld",
|
||||||
"bash-prompt-prefix": "(nix:$name)\\040",
|
"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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure existing plans still parse
|
// // 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.
|
// // 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")]
|
// #[cfg(target_os = "macos")]
|
||||||
#[test]
|
// #[test]
|
||||||
fn plan_compat_macos() -> eyre::Result<()> {
|
// fn plan_compat_macos() -> eyre::Result<()> {
|
||||||
let _: InstallPlan = serde_json::from_str(MACOS)?;
|
// let _: InstallPlan = serde_json::from_str(MACOS)?;
|
||||||
Ok(())
|
// Ok(())
|
||||||
}
|
// }
|
||||||
|
|
Loading…
Reference in a new issue