Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/action/macos/create_nix_volume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::{
use tracing::{Span, span};

use super::{
CreateVolumeService, DARWIN_LAUNCHD_DOMAIN, KickstartLaunchctlService,
CreateVolumeService, DARWIN_LAUNCHD_DOMAIN, KickstartLaunchctlService, SuppressVolumeIndexing,
create_fstab_entry::CreateFstabEntry,
};

Expand All @@ -39,6 +39,7 @@ pub struct CreateNixVolume {
bootstrap_volume: StatefulAction<BootstrapLaunchctlService>,
kickstart_launchctl_service: StatefulAction<KickstartLaunchctlService>,
enable_ownership: StatefulAction<EnableOwnership>,
suppress_indexing: StatefulAction<SuppressVolumeIndexing>,
}

impl CreateNixVolume {
Expand Down Expand Up @@ -96,6 +97,7 @@ impl CreateNixVolume {
KickstartLaunchctlService::plan(DARWIN_LAUNCHD_DOMAIN, NIX_VOLUME_MOUNTD_NAME)
.map_err(Self::error)?;
let enable_ownership = EnableOwnership::plan("/nix").map_err(Self::error)?;
let suppress_indexing = SuppressVolumeIndexing::plan("/nix").map_err(Self::error)?;

Ok(Self {
disk: disk.to_path_buf(),
Expand All @@ -112,6 +114,7 @@ impl CreateNixVolume {
bootstrap_volume,
kickstart_launchctl_service,
enable_ownership,
suppress_indexing,
}
.into())
}
Expand Down Expand Up @@ -154,6 +157,7 @@ impl Action for CreateNixVolume {
explanation.push(self.setup_volume_daemon.tracing_synopsis());
explanation.push(self.bootstrap_volume.tracing_synopsis());
explanation.push(self.enable_ownership.tracing_synopsis());
explanation.push(self.suppress_indexing.tracing_synopsis());

vec![ActionDescription::new(self.tracing_synopsis(), explanation)]
}
Expand Down Expand Up @@ -210,6 +214,7 @@ impl Action for CreateNixVolume {
crate::action::macos::wait_for_nix_store_dir().map_err(Self::error)?;

self.enable_ownership.try_execute().map_err(Self::error)?;
self.suppress_indexing.try_execute().map_err(Self::error)?;

Ok(())
}
Expand All @@ -228,6 +233,7 @@ impl Action for CreateNixVolume {
explanation.push(self.setup_volume_daemon.tracing_synopsis());
explanation.push(self.bootstrap_volume.tracing_synopsis());
explanation.push(self.enable_ownership.tracing_synopsis());
explanation.push(self.suppress_indexing.tracing_synopsis());

vec![ActionDescription::new(
format!(
Expand All @@ -243,6 +249,10 @@ impl Action for CreateNixVolume {
fn revert(&mut self) -> Result<(), ActionError> {
let mut errors = vec![];

if let Err(err) = self.suppress_indexing.try_revert() {
errors.push(err);
}

if let Err(err) = self.enable_ownership.try_revert() {
errors.push(err);
}
Expand Down
2 changes: 2 additions & 0 deletions src/action/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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 suppress_volume_indexing;
pub(crate) mod unmount_apfs_volume;

use std::fs;
Expand All @@ -34,6 +35,7 @@ pub use kickstart_launchctl_service::KickstartLaunchctlService;
use serde::Deserialize;
pub use set_tmutil_exclusion::SetTmutilExclusion;
pub use set_tmutil_exclusions::SetTmutilExclusions;
pub use suppress_volume_indexing::SuppressVolumeIndexing;
pub use unmount_apfs_volume::UnmountApfsVolume;

use crate::execute_command;
Expand Down
122 changes: 122 additions & 0 deletions src/action/macos/suppress_volume_indexing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use tracing::{Span, span};

use crate::action::{
Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction,
};
use crate::execute_command;

/**
Hide the Nix volume from Finder and stop fseventsd journaling on it.

On large Nix stores (hundreds of thousands of entries), Finder and the
system open/save panel XPC service enumerate `/nix/store` via the synthetic
firmlink under `/` and cache one `_FileCache` + `NSURL` object per entry in
`DesktopServicesPriv`, leaking on the order of a gigabyte of RSS and burning
CPU re-syncing on every fsevent. The `nobrowse` fstab option hides the
*volume* from the sidebar (and from Spotlight/mds), but not the firmlink
directory entry under `/` that Finder walks.

This action sets the BSD `UF_HIDDEN` flag on the mount point so directory
enumeration in Finder/NSOpenPanel skips it, and drops the Apple-documented
`.fseventsd/no_log` marker so fseventsd stops writing its event journal
during builds and GC. Spotlight needs no extra marker: `nobrowse` already
covers it, and `.metadata_never_index` is unreliable on recent macOS.
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
#[serde(tag = "action_name", rename = "suppress_volume_indexing")]
pub struct SuppressVolumeIndexing {
mount_point: PathBuf,
}

impl SuppressVolumeIndexing {
#[tracing::instrument(level = "debug", skip_all)]
pub fn plan(mount_point: impl AsRef<Path>) -> Result<StatefulAction<Self>, ActionError> {
Ok(Self {
mount_point: mount_point.as_ref().to_path_buf(),
}
.into())
}
}

#[typetag::serde(name = "suppress_volume_indexing")]
impl Action for SuppressVolumeIndexing {
fn action_tag() -> ActionTag {
ActionTag("suppress_volume_indexing")
}
fn tracing_synopsis(&self) -> String {
format!(
"Hide `{}` from Finder and disable fseventsd journaling on it",
self.mount_point.display()
)
}

fn tracing_span(&self) -> Span {
span!(
tracing::Level::DEBUG,
"suppress_volume_indexing",
mount_point = %self.mount_point.display(),
)
}

fn execute_description(&self) -> Vec<ActionDescription> {
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
}

#[tracing::instrument(level = "debug", skip_all)]
fn execute(&mut self) -> Result<(), ActionError> {
let fseventsd_dir = self.mount_point.join(".fseventsd");
fs::create_dir_all(&fseventsd_dir)
.map_err(|e| ActionErrorKind::CreateDirectory(fseventsd_dir.clone(), e))
.map_err(Self::error)?;
let no_log = fseventsd_dir.join("no_log");
fs::write(&no_log, b"")
.map_err(|e| ActionErrorKind::Write(no_log, e))
.map_err(Self::error)?;

execute_command(
Command::new("/usr/bin/chflags")
.arg("hidden")
.arg(&self.mount_point)
.stdin(std::process::Stdio::null()),
)
.map_err(Self::error)?;

Ok(())
}

fn revert_description(&self) -> Vec<ActionDescription> {
vec![ActionDescription::new(
format!(
"Remove fseventsd opt-out marker and unhide `{}`",
self.mount_point.display()
),
vec![],
)]
}

#[tracing::instrument(level = "debug", skip_all)]
fn revert(&mut self) -> Result<(), ActionError> {
// Best-effort: uninstall removes the volume anyway, so just log failures.
let no_log = self.mount_point.join(".fseventsd").join("no_log");
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to do a bit of research if there is anything that will stop working if we do this.

if let Err(err) = fs::remove_file(&no_log)
&& err.kind() != std::io::ErrorKind::NotFound
{
tracing::warn!(?err, path = %no_log.display(), "Could not remove fseventsd opt-out marker");
}

if let Err(err) = execute_command(
Command::new("/usr/bin/chflags")
.arg("nohidden")
.arg(&self.mount_point)
.stdin(std::process::Stdio::null()),
) {
tracing::warn!(?err, path = %self.mount_point.display(), "Could not clear hidden flag");
}

Ok(())
}
}
7 changes: 7 additions & 0 deletions tests/fixtures/macos/macos.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@
"path": "/nix"
},
"state": "Completed"
},
"suppress_indexing": {
"action": {
"action_name": "suppress_volume_indexing",
"mount_point": "/nix"
},
"state": "Completed"
}
},
"state": "Completed"
Expand Down