From 4a7f1afa622a85a2014e14c1ed4ea2979e276c3c Mon Sep 17 00:00:00 2001 From: Ethan Graf Date: Wed, 29 Apr 2026 21:47:33 -0400 Subject: [PATCH] Modify vendored fskit-rs to implement a alternate dev session backend --- Cargo.lock | 1 + Cargo.toml | 5 +- README.md | 33 ++++-------- VENDORED_FSKIT_RS_CHANGELOG.md | 46 ----------------- src/backend.rs | 43 ++++++++++++++++ src/backend/dev.rs | 39 ++++++++++++++ src/backend/prod.rs | 32 ++++++++++++ src/dev.rs | 5 -- src/main.rs | 51 +++++-------------- vendor/README.md | 4 +- .../fskit-rs/VENDORED_FSKIT_RS_CHANGELOG.md | 29 +++++++++++ vendor/fskit-rs/src/dev_session.rs | 46 +++++++++++++++++ vendor/fskit-rs/src/lib.rs | 14 +---- vendor/fskit-rs/src/session.rs | 5 +- 14 files changed, 219 insertions(+), 134 deletions(-) delete mode 100644 VENDORED_FSKIT_RS_CHANGELOG.md create mode 100644 src/backend.rs create mode 100644 src/backend/dev.rs create mode 100644 src/backend/prod.rs delete mode 100644 src/dev.rs create mode 100644 vendor/fskit-rs/VENDORED_FSKIT_RS_CHANGELOG.md create mode 100644 vendor/fskit-rs/src/dev_session.rs diff --git a/Cargo.lock b/Cargo.lock index 26f4ee0..212b1c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -771,6 +771,7 @@ dependencies = [ "fskit-rs", "libc", "tempfile", + "thiserror", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 2f63234..bec3927 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,14 +5,15 @@ edition = "2021" [features] default = [] -# Local development: extra logging and future dev-only tools (mock client, etc.). +# Local development: select the socket-only `DevBackend` (see src/backend/dev.rs). dev = [] [dependencies] async-trait = "0.1" env_logger = "0.11" -# Vendored fork: run ./scripts/bootstrap-vendor-fskit-rs.sh once (see VENDORED_FSKIT_RS_CHANGELOG.md). +# Vendored crate. See vendor/fskit-rs/VENDORED_FSKIT_RS_CHANGELOG.md. fskit-rs = { path = "vendor/fskit-rs" } libc = "0.2" tempfile = "3" +thiserror = "2" tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } diff --git a/README.md b/README.md index 48f0bd8..6640fd6 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,17 @@ # subtext -Stub FSKit backend: implements [`fskit-rs`](https://crates.io/crates/fskit-rs)’s `Filesystem` trait with `ENOSYS` for every call until you fill them in. +Rust FSKit backend stub: `Filesystem` returns `ENOSYS` until you implement it. -## Run (development) +The active backend is picked at compile time (`src/backend/`): -1. **Rust** (current stable) and **Protocol Buffers compiler** — `protoc` must be on your `PATH` (e.g. `brew install protobuf` on macOS; `fskit-rs` needs it to generate code). +- default → real FSKit mount via `fskit_rs::mount`. +- `--features dev` → socket-only daemon on `127.0.0.1:$SUBTEXT_DEV_PORT` (default `8080`); no FSKit, no `mount(8)`. -2. **Vendor `fskit-rs` (first clone / after changing the fork):** we use a path dependency on `vendor/fskit-rs` (see [`VENDORED_FSKIT_RS_CHANGELOG.md`](VENDORED_FSKIT_RS_CHANGELOG.md)). From the repo root: +**Prereqs:** stable Rust, `protoc` on `PATH` (e.g. `brew install protobuf`). - ```sh - cargo fetch -p fskit-rs@0.2.0 - chmod +x scripts/bootstrap-vendor-fskit-rs.sh scripts/apply_subtext_fskit_rs_edits.py - ./scripts/bootstrap-vendor-fskit-rs.sh - ``` +```sh +cargo build --features dev +cargo run --features dev +``` -3. From the repo root, use the **`dev`** feature for local work (enables dev-only hooks; add more under `#[cfg(feature = "dev")]` / `src/dev.rs`): - - ```sh - cargo build --features dev - cargo run --features dev - ``` - -4. **What running does:** the binary calls `fskit_rs::mount` (same idea as [`basic_fs`](https://github.com/debox-network/fskit-rs/blob/main/examples/basic_fs.rs)) with [`StubFilesystem`](src/lib.rs) and a **temporary** mount point under the system temp directory, then blocks until **Ctrl+C** and unmounts. With **`--features dev`**, the vendored `fskit-rs` uses `MountOptions::skip_registration` so the TCP server can start **without** PlugInKit finding a registered appex (see changelog); you still need a full FSKit stack for `mount` to succeed in production. Optional: `RUST_LOG=info` for more logging from `fskit-rs`. - -5. Without `--features dev`, `cargo run` is the same flow minus dev-only hooks and without skipping registration. A build **without** `--features dev` is the default “non-dev” binary (e.g. `cargo build --release` for production-like artifacts). - -## macOS - -FSKit integration targets **macOS 15.4+** and a signed FSKit bridge app; this repo is the Rust side only. +`fskit-rs` is vendored in `vendor/fskit-rs` (changelog there if you re-bootstrap from the registry). diff --git a/VENDORED_FSKIT_RS_CHANGELOG.md b/VENDORED_FSKIT_RS_CHANGELOG.md deleted file mode 100644 index 8252ff1..0000000 --- a/VENDORED_FSKIT_RS_CHANGELOG.md +++ /dev/null @@ -1,46 +0,0 @@ -# Vendored `fskit-rs` — subtext delta log - -This document records **when** we took code from upstream **fskit-rs**, **what** we changed in the vendored copy under `vendor/fskit-rs/`, and **why**. The mechanical edits live in `scripts/apply_subtext_fskit_rs_edits.py` (run for you by `scripts/bootstrap-vendor-fskit-rs.sh`). - -## Upstream snapshot - -| Field | Value | -|--------|--------| -| **Crate** | [`fskit-rs`](https://crates.io/crates/fskit-rs) **0.2.0** | -| **Registry source** | `~/.cargo/registry/src/index.crates.io-*/fskit-rs-0.2.0` (from `cargo fetch -p fskit-rs@0.2.0`) | -| **Git tag (reference)** | [`v0.2.0`](https://github.com/debox-network/fskit-rs/releases/tag/v0.2.0) on [debox-network/fskit-rs](https://github.com/debox-network/fskit-rs) | -| **Vendored in subtext (first import)** | **2026-04-28** (date this workflow was added; re-run `bootstrap-vendor-fskit-rs.sh` after upstream bumps) | - -We do **not** use a separate git remote for the vendored tree; it is a **file copy** of the published 0.2.0 sources plus the edits below. Re-applying the script over a fresh 0.2.0 copy is the supported way to reconcile with upstream. - -## Why vendoring instead of a public fork - -- We needed **one optional code path** without waiting for an upstream API (`MountOptions` / `Session::new` behavior). -- A private or long-lived **git fork** would work, but a **path vendored crate** keeps all consumer changes in the **subtext repo** with a **single script** to reproduce the tree from crates.io. - -## Changes from upstream 0.2.0 (subtext patches) - -### 1. `SkipRegistration` and `MountOptions::skip_registration` - -- **Files:** `src/lib.rs` -- **What:** Introduced a public `SkipRegistration { server_port, fs_type }` and added `pub skip_registration: Option` to `MountOptions`, with `Default` setting `skip_registration: None` (preserving upstream behavior when unset). -- **Why:** The stock `Session::new` always calls `read_config()`, which uses PlugInKit + the appex `Info.plist` to discover the TCP port and FS type. That **fails** when no FSKit host extension is installed or elected (typical in CI or when experimenting without a paid signing setup). An explicit override lets the TCP listener start using **known** `server_port` and `fs_type` (e.g. `35367` and `fskitbridge`) for local testing. **System `mount(8)` still runs afterward**; skipping registration does not replace a full FSKit stack for real mounts. - -### 2. Session: honor `skip_registration` before `read_config()` - -- **Files:** `src/session.rs` -- **What:** If `opts.skip_registration` is `Some`, use that port and `fs_type`; otherwise call `read_config(&opts.fskit_id)?` as before. -- **Why:** Same as above: optional bypass of registration/plist discovery only. - -## Reconciliation checklist (future you / upstream) - -- [ ] Re-fetch registry: `cargo fetch -p fskit-rs@0.2.0` (or a newer version if you bump). -- [ ] Re-run `./scripts/bootstrap-vendor-fskit-rs.sh`. -- [ ] If upstream adds an official equivalent API, **drop** the Python edits, switch `Cargo.toml` back to a crates.io version, and delete this vendor tree. - -## Scripts - -| Script | Role | -|--------|------| -| `scripts/bootstrap-vendor-fskit-rs.sh` | Copy 0.2.0 from the cargo registry into `vendor/fskit-rs/`, then apply edits. | -| `scripts/apply_subtext_fskit_rs_edits.py` | Idempotent string edits; **this is the source of truth** for the diff vs. pristine 0.2.0. | diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..635ba47 --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,43 @@ +//! Filesystem backend abstraction. +//! +//! At compile time the active backend is selected by Cargo feature: +//! - default → [`prod::ProdBackend`] (real FSKit mount via [`fskit_rs::mount`]). +//! - `dev` → [`dev::DevBackend`] (socket-only, no FSKit / no `mount(8)`). +//! +//! Both expose the same shape via the [`Backend`] trait, so [`crate::main`] +//! and any caller can be feature-agnostic. + +use async_trait::async_trait; +use fskit_rs::{Filesystem, MountOptions}; + +pub type Result = std::result::Result; + +#[async_trait] +pub trait Backend: Send + Sync + Sized + 'static { + /// Start the backend; returns once it's ready to accept requests. + async fn start(fs: FS, opts: MountOptions) -> Result + where + FS: Filesystem + Send + Sync + Clone + 'static; + + /// Short, user-facing description (mount point, socket address, ...). + fn describe(&self) -> String; +} + +#[cfg(not(feature = "dev"))] +mod prod; +#[cfg(not(feature = "dev"))] +pub use prod::ProdBackend as ActiveBackend; + +#[cfg(feature = "dev")] +mod dev; +#[cfg(feature = "dev")] +pub use dev::DevBackend as ActiveBackend; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Session(#[from] fskit_rs::session::Error), + + #[error(transparent)] + DevSession(#[from] fskit_rs::dev_session::Error), +} diff --git a/src/backend/dev.rs b/src/backend/dev.rs new file mode 100644 index 0000000..1173e3e --- /dev/null +++ b/src/backend/dev.rs @@ -0,0 +1,39 @@ +//! Dev backend: socket-only daemon. No FSKit, no `mount(8)`. +//! +//! Listens on `127.0.0.1:$SUBTEXT_DEV_PORT` (default `8080`). A client +//! (e.g. FSKitBridge or a local test harness) connects and drives the +//! [`Filesystem`] over the wire. + +use async_trait::async_trait; +use fskit_rs::dev_session::DevSession; +use fskit_rs::{Filesystem, MountOptions}; + +use super::{Backend, Result}; + +const DEFAULT_PORT: u16 = 8080; + +#[derive(Debug)] +pub struct DevBackend { + port: u16, + // Held for its `Drop` impl, which stops the socket. + _session: DevSession, +} + +#[async_trait] +impl Backend for DevBackend { + async fn start(fs: FS, _opts: MountOptions) -> Result + where + FS: Filesystem + Send + Sync + Clone + 'static, + { + let port = std::env::var("SUBTEXT_DEV_PORT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(DEFAULT_PORT); + let session = DevSession::new(fs, port).await?; + Ok(Self { port, _session: session }) + } + + fn describe(&self) -> String { + format!("dev socket on 127.0.0.1:{}", self.port) + } +} diff --git a/src/backend/prod.rs b/src/backend/prod.rs new file mode 100644 index 0000000..2622a2e --- /dev/null +++ b/src/backend/prod.rs @@ -0,0 +1,32 @@ +//! Production backend: drives a real FSKit mount through [`fskit_rs::mount`]. + +use std::path::PathBuf; + +use async_trait::async_trait; +use fskit_rs::session::Session; +use fskit_rs::{Filesystem, MountOptions}; + +use super::{Backend, Result}; + +#[derive(Debug)] +pub struct ProdBackend { + mount_point: PathBuf, + // Held for its `Drop` impl, which unmounts and stops the socket. + _session: Session, +} + +#[async_trait] +impl Backend for ProdBackend { + async fn start(fs: FS, opts: MountOptions) -> Result + where + FS: Filesystem + Send + Sync + Clone + 'static, + { + let mount_point = opts.mount_point.clone(); + let session = fskit_rs::mount(fs, opts).await?; + Ok(Self { mount_point, _session: session }) + } + + fn describe(&self) -> String { + format!("FSKit mount at {}", self.mount_point.display()) + } +} diff --git a/src/dev.rs b/src/dev.rs deleted file mode 100644 index c0de1af..0000000 --- a/src/dev.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Development-only code (built with `cargo run --features dev`). - -pub fn init() { - eprintln!("subtext: dev mode enabled"); -} diff --git a/src/main.rs b/src/main.rs index 3fd04b0..9c16f7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,56 +1,29 @@ -//! Binary entry point: mounts like [`fskit-rs`’s `basic_fs` example](https://github.com/debox-network/fskit-rs/blob/main/examples/basic_fs.rs). +//! Binary entry point. The active filesystem backend is picked at compile +//! time by Cargo features (see [`subtext::backend`]); this file is otherwise +//! feature-agnostic. -#[cfg(feature = "dev")] -mod dev; - -use fskit_rs::{session, MountOptions}; +use fskit_rs::MountOptions; use subtext::StubFilesystem; +use subtext::backend::{ActiveBackend, Backend}; use tokio::signal; #[tokio::main] -async fn main() -> session::Result<()> { +async fn main() -> Result<(), Box> { let _ = env_logger::try_init(); - #[cfg(feature = "dev")] - dev::init(); - - let handler = StubFilesystem::default(); + let fs = StubFilesystem::default(); let temp = tempfile::tempdir()?; let mut opts = MountOptions::default(); opts.mount_point = temp.path().to_path_buf(); - // Without a registered FSKit appex, use fixed port/type (must match FSKitBridge when testing for real). - #[cfg(feature = "dev")] - { - opts.skip_registration = Some(fskit_rs::SkipRegistration { - server_port: 35367, - fs_type: "fskitbridge".into(), - }); - } - - println!( - "Mounting example filesystem at {}...", - opts.mount_point.display() - ); - - let session = match fskit_rs::mount(handler, opts.clone()).await { - Ok(session) => session, - Err(err) => { - eprintln!("Mount failed. Ensure the FSKit host app is installed and enabled."); - return Err(err); - } - }; - - println!( - "Mounted. Press Ctrl+C to unmount {}.", - opts.mount_point.display() - ); - signal::ctrl_c().await?; + let backend = ActiveBackend::start(fs, opts).await?; + println!("Started: {}. Press Ctrl+C to stop.", backend.describe()); - drop(session); + signal::ctrl_c().await?; - println!("Unmounted {}.", opts.mount_point.display()); + drop(backend); + println!("Stopped."); Ok(()) } diff --git a/vendor/README.md b/vendor/README.md index b085a54..b630c5c 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -1,6 +1,6 @@ # Vendored `fskit-rs` -This directory holds a **copy of [`fskit-rs` 0.2.0](https://crates.io/crates/fskit-rs/0.2.0)** from your local Cargo registry, with **small subtext-specific changes** (see `../VENDORED_FSKIT_RS_CHANGELOG.md`). +This directory holds a **copy of [`fskit-rs` 0.2.0](https://crates.io/crates/fskit-rs/0.2.0)** from your local Cargo registry, with **small subtext-specific changes** (see `fskit-rs/VENDORED_FSKIT_RS_CHANGELOG.md`). ## Populate or refresh @@ -10,7 +10,7 @@ From the repository root (after `cargo fetch -p fskit-rs@0.2.0` if needed): ./scripts/bootstrap-vendor-fskit-rs.sh ``` -This copies `~/.cargo/registry/src/.../fskit-rs-0.2.0` to `vendor/fskit-rs` and runs `scripts/apply_subtext_fskit_rs_edits.py`. +This copies `~/.cargo/registry/src/.../fskit-rs-0.2.0` to `vendor/fskit-rs`. Re-apply subtext edits manually and update `fskit-rs/VENDORED_FSKIT_RS_CHANGELOG.md`. ## Cargo layout diff --git a/vendor/fskit-rs/VENDORED_FSKIT_RS_CHANGELOG.md b/vendor/fskit-rs/VENDORED_FSKIT_RS_CHANGELOG.md new file mode 100644 index 0000000..051a102 --- /dev/null +++ b/vendor/fskit-rs/VENDORED_FSKIT_RS_CHANGELOG.md @@ -0,0 +1,29 @@ +# Vendored `fskit-rs` — subtext delta log + +Records what changed in `vendor/fskit-rs/` relative to upstream and why. + +## Upstream snapshot + +| Field | Value | +|--------|--------| +| Crate | [`fskit-rs`](https://crates.io/crates/fskit-rs) **0.2.0** | +| Registry source | `~/.cargo/registry/src/index.crates.io-*/fskit-rs-0.2.0` (from `cargo fetch -p fskit-rs@0.2.0`) | +| Git tag | [`v0.2.0`](https://github.com/debox-network/fskit-rs/releases/tag/v0.2.0) at [debox-network/fskit-rs](https://github.com/debox-network/fskit-rs) | +| First imported | 2026-04-28 | + +This is a file copy of upstream 0.2.0 with the additive change below — no separate git remote, no patch script. + +## Change set + +### 1. Add `dev_session` module (additive) + +- **Files:** new `src/dev_session.rs`; one new line in `src/lib.rs` (`pub mod dev_session;`). +- **What:** Exposes `DevSession` and `dev_session::Error`. `DevSession::new(fs, port)` starts the same `Socket` + `Handler` that `session::Session` uses, but skips PlugInKit / `Info.plist` discovery and `Mounter::mount`. `Drop` stops the socket. +- **Why:** Lets a caller exercise a `Filesystem` impl over the wire without a registered FSKit appex (no Apple Developer Program, no signed bridge app). Upstream files are untouched, so no `dead_code` or feature-gating sprawl in `session.rs` / `mounter.rs` / `info.rs`. + +## Reconciliation + +- Re-fetch: `cargo fetch -p fskit-rs@0.2.0` +- Re-bootstrap: `./scripts/bootstrap-vendor-fskit-rs.sh` (copies pristine 0.2.0 over the tree) +- Re-add `vendor/fskit-rs/src/dev_session.rs` and the `pub mod dev_session;` line. +- If upstream ever ships an equivalent socket-only entry point, drop the vendored delta and return to crates.io. diff --git a/vendor/fskit-rs/src/dev_session.rs b/vendor/fskit-rs/src/dev_session.rs new file mode 100644 index 0000000..a33aa80 --- /dev/null +++ b/vendor/fskit-rs/src/dev_session.rs @@ -0,0 +1,46 @@ +//! Socket-only session for local development. +//! +//! [`DevSession`] is a parallel entry point to [`super::session::Session`] that +//! starts the same TCP server (same protocol, same `Handler`) but skips +//! PlugInKit / `Info.plist` discovery and the `mount(8)` orchestration done by +//! [`super::mounter::Mounter`]. This lets a `Filesystem` impl be exercised over +//! the wire (e.g. with a separate FSKitBridge / test client) without requiring +//! a registered FSKit appex on the host. +//! +//! Added in subtext's vendored copy; not part of upstream `fskit-rs 0.2.0`. + +use super::Filesystem; +use super::handler::Handler; +use super::socket::{self, Socket}; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub struct DevSession { + socket: Socket, +} + +impl DevSession { + pub async fn new(fs: FS, server_port: u16) -> Result + where + FS: Filesystem + Send + Sync + Clone + 'static, + { + let handler = Handler::new(fs); + let socket = Socket::start(handler, server_port).await?; + Ok(Self { socket }) + } +} + +impl Drop for DevSession { + fn drop(&mut self) { + futures::executor::block_on(async { + self.socket.stop().await; + }); + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Socket(#[from] socket::Error), +} diff --git a/vendor/fskit-rs/src/lib.rs b/vendor/fskit-rs/src/lib.rs index 8d3a908..8d5d3b8 100644 --- a/vendor/fskit-rs/src/lib.rs +++ b/vendor/fskit-rs/src/lib.rs @@ -15,6 +15,7 @@ pub use crate::pb::{ }; use crate::session::Session; +pub mod dev_session; mod handler; mod info; pub mod installer; @@ -202,23 +203,11 @@ pub enum Error { /// (may require `sudo`) or a user-owned path. Default: `/tmp/fskitbridge`. /// * `force` — If `true`, preflight **unmounts** anything already mounted at `mount_point` /// before mounting. Default: `true`. -/// * `skip_registration` — If set, skip PlugInKit / appex `Info.plist` discovery and use the -/// given TCP port and FS type for the listener and for `mount -F -t`. For local testing -/// without a registered FSKit extension. Default: `None`. -#[derive(Debug, Clone)] -pub struct SkipRegistration { - /// Port bound as `127.0.0.1:` (e.g. `35367` for stock FSKitBridge). - pub server_port: u16, - /// Filesystem type passed to `mount -F -t `. - pub fs_type: String, -} - #[derive(Debug, Clone)] pub struct MountOptions { pub fskit_id: String, pub mount_point: PathBuf, pub force: bool, - pub skip_registration: Option, } impl Default for MountOptions { @@ -227,7 +216,6 @@ impl Default for MountOptions { fskit_id: FSKIT_ID.into(), mount_point: PathBuf::from(DEFAULT_MOUNT_POINT), force: true, - skip_registration: None, } } } diff --git a/vendor/fskit-rs/src/session.rs b/vendor/fskit-rs/src/session.rs index fa29044..e225416 100644 --- a/vendor/fskit-rs/src/session.rs +++ b/vendor/fskit-rs/src/session.rs @@ -24,10 +24,7 @@ impl Session { where FS: Filesystem + Send + Sync + Clone + 'static, { - let (server_port, fs_type) = match &opts.skip_registration { - Some(skip) => (skip.server_port, skip.fs_type.clone()), - None => read_config(&opts.fskit_id)?, - }; + let (server_port, fs_type) = read_config(&opts.fskit_id)?; let handler = Handler::new(fs); -- 2.51.2