Wick
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 thefusermount3userspace helper,enif_select-based readiness notifications, and a bounded read/write API for protocol frames. NoCAP_SYS_ADMINneeded. - 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.