FUSE Composition Layer
The FUSE composition layer provides a unified filesystem view of repositories and their dependencies. This document covers the architecture for developers extending or maintaining the composition system.
Architecture Overview
┌────────────────────────────────────────────────────────────────┐
│ CompositionBackend trait │
├────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ FUSE Backend │ │ Symlink Backend │ │
│ │ (Development) │ │ (CI / Fallback) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │ │ │
│ └───────────┬───────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Composition API │ │
│ │ (shared interface) │ │
│ └───────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
Core Components
Backend Trait
The CompositionBackend trait (src/rust/composition/src/backend.rs) defines
the interface for all backends:
#![allow(unused)] fn main() { pub trait CompositionBackend: Send + Sync { fn mount(&mut self) -> Result<()>; fn unmount(&mut self) -> Result<()>; fn status(&self) -> BackendStatus; fn cell_path(&self, cell: &str) -> Option<PathBuf>; fn refresh(&mut self) -> Result<()>; } }
Backend Selection
The selector (src/rust/composition/src/selector.rs) automatically chooses the
appropriate backend:
#![allow(unused)] fn main() { pub fn select_backend(requested: BackendType) -> BackendSelection { match requested { BackendType::Auto => { if is_fuse_available() { BackendSelection::fuse("Auto-selected FUSE") } else { BackendSelection::symlink("Auto-selected symlinks (FUSE unavailable)") } } BackendType::Fuse => { /* ... */ } BackendType::Symlink => { /* ... */ } } } }
State Machine
The consistency state machine (src/rust/composition/src/state.rs) manages
transitions:
Settled ──manifest change──► Syncing ──nix build──► Building
▲ │
│ build done
│ │
└───────────────────── Transitioning ◄───────────────┘
Key types:
ConsistencyStateMachine- Thread-safe state managementStateObserver- Trait for state change notificationsCellUpdate- Pending cell updates during transitions
Policy System
The policy system (src/rust/composition/src/policy.rs) controls access during
updates. See FUSE Access Policy for details.
Layout System
Layouts (src/rust/composition/src/layout.rs) control how files are presented:
Layouttrait - Core interface for layoutsLayoutRegistry- Runtime layout registrationBuck2Layout- Default Buck2 layoutBazelLayout- Bazel layout
See Custom Layouts for creating new layouts.
Module Structure
src/rust/nix-eval/src/ # Nix client abstraction (replaceable)
├── lib.rs # NixClient trait
├── cli.rs # CliNixClient (shells out to nix binary)
└── error.rs
src/rust/composition/src/
├── lib.rs # Public API exports
├── backend.rs # CompositionBackend trait
├── compose_config.rs # compose.toml parser (single-mount legacy)
├── config.rs # CompositionConfig, CellConfig
├── discover.rs # Cell discovery via NixClient trait
├── error.rs # Error types
├── layout.rs # Layout system (Buck2Layout, BazelLayout)
├── policy.rs # Access policies
├── recovery.rs # Error recovery utilities
├── selector.rs # Backend selection logic
├── serve_config.rs # Service config ([[mounts]] TOML format)
├── service.rs # Launchd/systemd service generation
├── state.rs # Consistency state machine
├── status.rs # BackendStatus enum
├── symlink.rs # Symlink backend
├── synthetic.rs # macOS synthetic firmlink management
├── tracing.rs # Logging and debugging
├── watcher.rs # File watching (optional)
└── fuse/ # FUSE backend (feature-gated)
├── mod.rs # Re-exports FuseBackend (platform-conditional)
├── fs_core.rs # Platform-agnostic filesystem logic
├── platform.rs # Platform detection and FUSE availability
├── filesystem.rs # Linux: fuser Filesystem trait impl
├── backend.rs # Linux: FuseBackend using fuser crate
├── edit_overlay.rs # Copy-on-write editing layer
├── patch_generator.rs # Unified diff generation for edits
└── fuse_t/ # macOS: direct libfuse-t backend
├── mod.rs
├── bindings.rs # Hand-written FFI bindings to libfuse3
├── operations.rs # FUSE operation callbacks (path-based API)
└── backend.rs # FuseTBackend implementing CompositionBackend
nix/home-manager/
└── turnkey-composed.nix # Home-manager module for service management
Daemon Architecture
The turnkey-composed daemon supports two modes:
start: Single mount, ad-hoc usageserve: Multi-mount service mode, reads config file, watches for changes
In service mode, the daemon:
- Reads
~/.config/turnkey/composed.tomlfor mount declarations - For each mount: discovers cells via
nix-evalcrate, builds them, creates the FUSE mount - Watches manifest files for dependency changes (triggers cell rebuild)
- Watches the config file for new/removed mounts (hot-reload)
- On macOS, manages synthetic firmlinks for mount points under
/
Nix Integration
Cell derivations are exposed as flake packages (godeps-cell,
rustdeps-cell, etc.) by the flake-parts module. The daemon builds them
via the NixClient trait (currently CliNixClient which shells out to
nix). This abstraction allows replacing the CLI with a direct Nix daemon
client when one becomes available.
FUSE Backend Implementation
The FUSE backend uses a layered architecture with a platform-agnostic core and platform-specific adapters.
FsCore (Platform-Agnostic)
FsCore (fs_core.rs) contains all filesystem logic with zero dependency on
the fuser crate:
- Path resolution:
resolve_path(path) -> ResolvedPathmaps FUSE paths to logical locations (Root, Source, CellPrefix, Cell, VirtualFile, etc.) - Inode management: Allocation, mapping, and lookup using plain
u64inode numbers - Virtual file generation:
.buckconfigand.buckrootcontent - Policy checking: Access control during dependency updates
- Edit overlay: Copy-on-write editing of external dependencies
Both the Linux and macOS backends delegate to FsCore for all filesystem logic,
converting between their own FUSE types and FsCore's neutral types.
Linux Backend (fuser crate)
Uses the fuser crate's low-level inode-based API:
CompositionFswrapsFsCoreand implementsfuser::Filesystem- Converts between
fuser::INodeNo/FileAttrand FsCore'su64/FsAttr - Feature flag:
fuse(enablesdep:fuser)
macOS Backend (FUSE-T FFI)
Uses direct C FFI to FUSE-T's libfuse3, bypassing the fuser crate entirely.
This is necessary because fuser reads the FUSE file descriptor directly, which
is incompatible with FUSE-T's NFS-based socket protocol.
bindings.rs: Hand-written FFI bindings to libfuse3 (44-fieldfuse_operationsstruct at 352 bytes,fuse_new,fuse_mount,fuse_loop, etc.)operations.rs:extern "C"callbacks using the high-level path-based API. Each callback retrievesFsCorevia a globalAtomicPtrand delegates toresolve_path()backend.rs:FuseTBackendspawns a thread callingfuse_new+fuse_mount+fuse_loop- Feature flag:
fuse-t(onlydep:libcneeded) - Links against
/usr/local/lib/libfuse3.dylib(from FUSE-T)
FUSE-T quirks discovered during implementation:
fuse_get_context()->private_datadoes not reliably pass theuser_datafromfuse_new. A globalAtomicPtr<FsCore>is used instead.readdirfiller must pass null for the stat buffer. FUSE-T's NFS translation rejects certain stat formats with "RPC struct is bad".- The
fuse_operationsstruct must include the newerstatxandsyncfsfields even if unused, to match the 352-byte C ABI.
Conditional Compilation
Platform selection happens at compile time:
#![allow(unused)] fn main() { // In fuse/mod.rs: #[cfg(target_os = "linux")] pub use backend::FuseBackend; // fuser-based #[cfg(target_os = "macos")] pub use fuse_t::backend::FuseTBackend as FuseBackend; // libfuse-t FFI }
The selector.rs gates on #[cfg(any(feature = "fuse", feature = "fuse-t"))]
so both feature flags enable the FUSE code path.
Platform Detection
Runtime FUSE availability checking in platform.rs:
- Linux: Checks for
/dev/fuse - macOS: Checks for FUSE-T bundle (
/Library/Filesystems/fuse-t.fs) or library (/usr/local/lib/libfuse-t.dylib)
Recovery System
The recovery module (src/rust/composition/src/recovery.rs) provides:
Retry Logic
#![allow(unused)] fn main() { pub async fn retry_with_backoff<T, F, Fut>( config: &RetryConfig, operation: F, ) -> Result<T> where F: Fn() -> Fut, Fut: Future<Output = Result<T>>, }
Error Classification
#![allow(unused)] fn main() { pub fn is_transient_error(error: &Error) -> bool { matches!(error, Error::Timeout(_) | Error::PathUpdating(_) | ...) } }
Recovery Actions
#![allow(unused)] fn main() { pub enum RecoveryAction { Retry { delay: Duration }, ForceUnmount, RestartDaemon, ManualIntervention { instructions: String }, } }
Tracing and Debugging
The tracing module (src/rust/composition/src/tracing.rs) provides:
Configuration
#![allow(unused)] fn main() { pub struct TracingConfig { pub enable_fuse_ops: bool, pub enable_state_transitions: bool, pub enable_metrics: bool, pub log_level: String, } }
State Logger
Implements StateObserver to log state transitions:
#![allow(unused)] fn main() { impl StateObserver for StateLogger { fn on_state_change(&self, from: SystemState, to: SystemState) { info!("State: {:?} -> {:?}", from, to); } } }
Metrics
Tracks performance metrics:
- Operation counts (lookup, read, readdir, etc.)
- Latency histograms
- Cache hit rates
Debug Information
#![allow(unused)] fn main() { pub struct DebugInfo { pub backend_type: String, pub mount_point: Option<PathBuf>, pub cells: Vec<CellDebugInfo>, pub state: SystemState, pub metrics: Option<Metrics>, } }
Testing
Unit Tests
Each module has unit tests:
cargo test -p composition
Integration Tests
Test with actual FUSE mounts (requires FUSE):
# Linux
cargo test -p composition --features fuse -- --ignored
# macOS (FUSE-T)
cargo test -p composition --features fuse-t -- --ignored
Mock Backend
For testing without FUSE:
#![allow(unused)] fn main() { use composition::testing::MockBackend; let backend = MockBackend::new() .with_cell("godeps", "/nix/store/xxx-godeps") .with_status(BackendStatus::Ready); }
Feature Flags
The crate uses feature flags:
[features]
default = []
fuse = ["fuser"] # Enable FUSE backend
watcher = ["notify"] # Enable file watching
Error Handling
The Error enum in error.rs covers all failure modes:
#![allow(unused)] fn main() { pub enum Error { AlreadyMounted(PathBuf), NotMounted, MountPointInaccessible { path, source }, CellNotFound(String), FuseUnavailable(String), // ... } }
Errors include recovery suggestions:
#![allow(unused)] fn main() { impl Error { pub fn is_transient(&self) -> bool { /* ... */ } pub fn recovery_suggestion(&self) -> Option<String> { /* ... */ } } }
Configuration
The CompositionConfig struct holds all settings:
#![allow(unused)] fn main() { pub struct CompositionConfig { pub mount_point: PathBuf, pub cells: HashMap<String, CellConfig>, pub consistency_mode: ConsistencyMode, pub layout: String, } pub struct CellConfig { pub source_path: PathBuf, pub editable: bool, } }
Related Documentation
- FUSE Access Policy - Access control during updates
- Custom Layouts - Creating build system layouts
- Architecture Proposal - Original design document