A standalone Elixir library for building FUSE userspace filesystems on the BEAM — without libfuse bindings or a native event loop. The only native code is a minimal syscall NIF; everything above the file descriptor (frame parsing, protocol encoding, your filesystem logic) is ordinary supervised Elixir.

Two layers:

  • Transport (Wick.Native, Wick.Fusermount) — opens /dev/fuse, mounts via the fusermount3 userspace helper, enif_select-based readiness notifications, and a bounded read/write API for protocol frames. No CAP_SYS_ADMIN needed.
  • Codec (Wick.Protocol) — a pure-Elixir codec for the Linux FUSE kernel protocol (FUSE_KERNEL_VERSION 7.31, as exposed by libfuse 3.10+ / Linux 5.4+). Operates on binaries only — no I/O, so it is testable without a kernel in sight.

Installation

Add wick to your list of dependencies in mix.exs:

def deps do
  [{:wick, "~> 0.1.0"}]
end

Wick compiles a small Rust NIF via Rustler, so a Rust toolchain must be available at build time.

Writing a filesystem

A FUSE server is an event loop: mount, wait for a readiness notification, read a request frame, decode it, write a reply, re-arm, repeat. The kernel's first request is always INIT, and nothing else works until you answer it.

The Writing a filesystem guide builds a complete read-only filesystem from scratch and is the best place to start. The primitive below shows the raw transport and codec call sequence those servers are built from.

Mount and serve

{:ok, handle} =
  Wick.Fusermount.mount(
    "/tmp/my-mount",
    ["fsname=demo", "subtype=demo", "default_permissions"]
  )

:ok = Wick.Native.select_read(handle)

receive do
  {:select, ^handle, :undefined, :ready_input} ->
    {:ok, request_bytes} = Wick.Native.read_frame(handle)
    {:ok, op, header, request} = Wick.Protocol.decode_request(request_bytes)
    # ... build a reply struct for `op` ...
    response_bytes = Wick.Protocol.encode_response(header.unique, reply, 0)
    :ok = Wick.Native.write_frame(handle, response_bytes)
end

:ok = Wick.Fusermount.unmount("/tmp/my-mount")

Wick.Fusermount.mount/2 calls into a NIF that uses posix_spawn(3) to run fusermount3 with one end of a socketpair(2) inherited as fd 3, then receives the resulting /dev/fuse fd via SCM_RIGHTS. Wick.Fusermount.unmount/1 invokes fusermount3 -u via an Erlang Port so the BEAM's child-process management reaps the helper without colliding with SIGCHLD = SIG_IGN.

See Wick.Native, Wick.Fusermount, and Wick.Protocol for full documentation.

Tests without /dev/fuse

CI hosts that lack FUSE support can still exercise the transport:

{:ok, {read_fd, write_fd}} = Wick.Native.pipe_pair()

returns a non-blocking pipe pair wrapped in the same resource type, so the select_read / read_frame / write_frame path can be driven end-to-end. Tests that exercise Wick.Fusermount.mount/2 are tagged :fuse and skipped on hosts where /dev/fuse is not available.

GitHub Mirror

Eventually, Forgejo will support fully federated operation, but for now there's a mirror of this repository on GitHub - feel free to open issues and PRs there.

Licence

Apache-2.0 — see LICENSE for details.