Skip to content

About

High-performance TRAMP backend using JSON-RPC instead of shell parsing

Resources

Stars

351 stars

Watchers

6 watching

Forks

Repository files navigation

TRAMP-RPC

A high-performance TRAMP backend for Emacs that uses a binary RPC server instead of parsing shell command output.

Overview

Traditional TRAMP works by piping shell commands over SSH and parsing their output. This approach is robust but slow, especially for operations that require many round-trips (like directory listings or VC operations).

TRAMP-RPC replaces this with a lightweight Rust server that runs on the remote host. Emacs communicates with it using MessagePack-RPC over SSH, resulting in significantly faster file operations.

Why TRAMP-RPC?

AspectOriginal TRAMPTRAMP-RPC
CommunicationShell commands + parsingMessagePack-RPC protocol
LatencyMultiple round-tripsSingle round-trip
BatchingNot supportedMultiple ops per request
Shell dependencyRequired on remoteNot needed
Binary requiredNone~850KB Rust server

Features

  • Fast file operations via binary RPC protocol (2-38x faster than shell-based TRAMP)
  • Async process support (make-process, start-file-process)
  • Full VC mode integration (git, etc.)
  • Magit/Projectile optimizations with parallel git command prefetch
  • Majutsu log refresh batching with a shared jj working-copy snapshot
  • Automatic binary deployment (download or build from source)
  • Support for Linux and macOS (x86_64 and aarch64)
  • Batch/pipelined requests for reduced round-trip latency
  • Multi-hop support via SSH ProxyJump
  • Filesystem watching with automatic cache invalidation
  • PTY support for terminal emulators (vterm, eat)

Requirements

  • Emacs 30.1 or later
  • Tramp 2.8.1.4 or later (install from GNU ELPA if your Emacs bundles an older version)
  • msgpack.el 0.1.1 or later (installed automatically from MELPA)
  • SSH access to remote hosts
  • Remote host running Linux or macOS (x86_64 or aarch64)

Installation

From NonGNU ELPA (coming soon)

(use-package tramp-rpc
  :ensure t)

From Git (Emacs 30+)

(use-package tramp-rpc
  :after tramp
  :vc (:url "https://github.com/ArthurHeymans/emacs-tramp-rpc"
       :rev :newest
       :lisp-dir "lisp"))

Manual Installation

  1. Clone this repository:
    git clone https://github.com/ArthurHeymans/emacs-tramp-rpc.git
        
  2. Add to your Emacs init file:
    (add-to-list 'load-path "/path/to/emacs-tramp-rpc/lisp")
    (require 'tramp-rpc)
        

For Doom Emacs

  1. In packages.el:
    (package! msgpack)
    (package! tramp-rpc :recipe (:host github :repo "ArthurHeymans/emacs-tramp-rpc" :files ("lisp/*.el")))
        
  2. In config.el:
    (use-package! msgpack)
    (use-package! tramp-rpc)
        

From Nix flake

  1. Include this repository as a flake input and add its overlay to nixpkgs:
    inputs = {
      nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
      emacs-tramp-rpc.url = "github:ArthurHeymans/emacs-tramp-rpc";
    };
    
    outputs = inputs@{ nixpkgs, emacs-tramp-rpc, ... }:
      let
        system = "x86_64-linux";
        inherit (nixpkgs) lib;
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ emacs-tramp-rpc.overlays.default ];
        };
      in ...;
        
  2. Include tramp-rpc as you would any Elisp package from the emacsPackages scope, e.g. in Home Manager’s programs.emacs.extraPackages:
    programs.emacs.extraPackages = epkgs: [ epkgs.tramp-rpc ];
        
  3. Server binaries for x86_64-linux and aarch64-linux are automatically built and included in the Emacs package derivation; if you would like to change which systems are built, override the archs input:
    programs.emacs.extraPackages = epkgs: [
      (epkgs.tramp-rpc.override {
        archs = [
          pkgs.pkgsCross.riscv64-musl
          (import nixpkgs {
            inherit system;
            crossSystem = lib.systems.elaborate {
              config = "armv6l-unknown-linux-musleabihf";
            };
          })
        ];
      })
    ];
        

Usage

Access remote files using the rpc method:

/rpc:user@host:/path/to/file

SSH host aliases and ProxyJump routes in ~/.ssh/config work with /rpc:user@final-host:/path without adding a TRAMP hop: the RPC transport uses OpenSSH’s configuration for final-host. A remote exit code 127 can mean that the RPC server binary is missing; for a git checkout, run M-x tramp-rpc-deploy-install-binary and enter the complete TRAMP name, for example /rpc:user@final-host:/, to obtain and deploy it.

On first connection, the server binary is automatically obtained and deployed:

  1. Release/package installs: download from GitHub Releases first (fastest, ~850KB download), then build from source if Rust 1.88 or later is installed and download fails.
  2. Git checkout installs: reuse a fresh source build or source-tree keyed cache. Automatic file operations do not prompt when a binary is missing; run M-x tramp-rpc-deploy-install-binary to choose whether to download a checksum-verified release binary, build the checkout with Cargo, or skip. A downloaded fallback remains keyed to that source tree, while strict build policy uses a separate identity.

The binary is cached locally in ~/.emacs.d/tramp-rpc/ and deployed to ~/.cache/emacs/tramp-rpc/ on the remote host.

Multi-hop (|) file names

TRAMP’s ad-hoc multi-hop syntax works with the rpc method:

  • /rpc:user@host|sudo:root@host:/path stays RPC-accelerated: the RPC server on host is started via sudo, so file operations run with root privileges at full RPC speed.
  • Chains ending in shell-based methods such as su or docker, e.g. /rpc:host|su::/path or /rpc:host|docker:container:/path, use TRAMP’s regular shell-based handler, which logs in through the rpc hop as if it were ssh. The RPC server and its speedup are not used for these chains. RPC-to-RPC chains remain RPC-backed at the final target; intermediate rpc hops provide SSH routing.

Hidden routes in tramp-default-proxies-alist also apply to ordinary rpc targets, as with upstream TRAMP. Invalid proxy configurations signal an error rather than silently connecting directly.

Deployment Commands

CommandDescription
M-x tramp-rpc-deploy-install-binaryChoose and deploy a missing git-checkout binary; use C-u to replace one
M-x tramp-rpc-deploy-statusShow binary deployment status
M-x tramp-rpc-deploy-clear-cacheClear local binary cache
M-x tramp-rpc-deploy-remove-binaryRemove binary from remote

Architecture

┌─────────────┐   SSH/MessagePack-RPC  ┌──────────────────┐
│   Emacs     │ ◄────────────────────► │ tramp-rpc-server │
│ (tramp-rpc) │                        │     (Rust)       │
└─────────────┘                        └──────────────────┘

Module Organization

The Emacs Lisp client is organized into focused modules:

The modules form a strict layering; each requires only the ones below it. Where a lower layer needs to reach up, it runs a hook that the upper layer registers on at load time. The only declare-function back-references left are for functions emitted by the autoload form and for the file-notify dispatch callbacks in tramp-rpc-notify.el.

ModulePurpose
tramp-rpc.elTRAMP file handlers and registration
tramp-rpc-magit.elMagit/Projectile prefetch and process-file cache
tramp-rpc-majutsu.elMajutsu refresh-local query batching and root memo
tramp-rpc-advice.elAdvice functions for process/VC integration
tramp-rpc-process.elAsync process & PTY support
tramp-rpc-notify.elPublic file-notify descriptors on server watches
tramp-rpc-cache.elMetadata caches & filesystem watch notifications
tramp-rpc-transport.elSSH/ControlMaster, connection generations, RPC calls
tramp-rpc-deploy.elBinary deployment & version management
tramp-rpc-hops.elHop-chain analysis and sudo elevation
tramp-rpc-connection.elThe per-generation tramp-rpc-connection struct
tramp-rpc-protocol.elMessagePack-RPC framing, value helpers, debug logging

Core Module (tramp-rpc.el)

The core module provides:

  • All TRAMP file handler operations (file-exists-p, file-attributes, insert-file-contents, etc.)
  • File name handler registration, dispatch, and unload

Notify Module (tramp-rpc-notify.el)

Public file-notify descriptors (file-notify-add-watch and friends) on top of the server watches managed by the cache module.

Transport Module (tramp-rpc-transport.el)

Everything needed to reach a host and talk to its server:

  • SSH ControlMaster and multi-hop (ProxyJump) setup, authentication via tramp-process-actions
  • Server process startup, including sudo-via-RPC privilege elevation
  • Connection generation lookup, replacement and cleanup
  • RPC call primitives: synchronous, batch, pipelined and asynchronous calls
  • The effective remote process environment (PATH, login shell, direnv)

Upper layers plug into the transport through hooks instead of being called by name: tramp-rpc-connection-invalidate-functions, tramp-rpc-transport-terminate-functions, tramp-rpc-transport-cleanup-functions and tramp-rpc-notification-functions.

Connection Module (tramp-rpc-connection.el)

Defines tramp-rpc-connection, the object describing one transport generation: the SSH process, its response buffer, and all request state belonging to exactly that generation (pending synchronous IDs, buffered responses, async callbacks, and cleanup flags). A replacement connection for the same host is a new generation and can never observe the old one’s state. The transport process carries a single back pointer to its generation for use by process filters and sentinels.

Process Module (tramp-rpc-process.el)

Handles all process-related functionality:

  • Async pipe processes (make-process, start-file-process)
  • RPC-based PTY processes for terminal emulators
  • Terminal resize handling for vterm/eat/shell-mode
  • Process I/O queuing with server-pushed output and exit notifications

Cache Module (tramp-rpc-cache.el)

Client-side metadata caches and the watch machinery keeping them fresh:

  • TTL-based caches for file-exists-p, file-truename and file.stat with max-size eviction
  • Invalidation by path, subtree, and connection
  • Watch management via server push notifications (watch.add~/~watch.remove, fs.events)
  • Dispatch of fs.events to public file-notify descriptors

Modules with caches derived from file metadata register on tramp-rpc-cache-invalidate-functions and tramp-rpc-fs-events-functions; this is how the Magit module drops its ancestor scans and prefetched status without the cache module knowing about it.

Access predicates cache answers from the remote kernel, not guesses from mode bits or group lists. Entries distinguish connection routes and trailing slashes, while obeying TRAMP’s remote-file-name-inhibit-cache and watch invalidation. access-file always checks fresh read access and raises native Emacs error classes for filesystem denials. Writable checks of missing targets also test write/search access to the immediate parent in the same RPC, only after an ENOENT target lookup. Like native Emacs, file-accessible-directory-p checks whether a directory is searchable; read permission is only needed to list its names. New login group memberships require reconnecting the RPC server. Supplementary-group introspection is not provided; access decisions come from the kernel.

Magit Module (tramp-rpc-magit.el)

Optimizations for git/magit/projectile on remote hosts:

  • Parallel git command prefetch via commands.run_parallel RPC (sends ~60+ git commands in a single round-trip)
  • Process-file cache for serving git commands from prefetched data
  • Ancestor directory scanning for fast project/VC root detection
  • Projectile integration (force git ls-files, alien indexing for remote)

Majutsu Module (tramp-rpc-majutsu.el)

When Majutsu is loaded, RPC log refreshes prefetch the enabled built-in log, status, workspace and conflict sections. A jj op log command snapshots the working copy and returns a concrete operation ID; log, workspace and conflict queries run in parallel at that operation via commands.run_parallel. Status runs without operation pinning so files refused by auto-tracking or size limits remain visible. Exact command results, including separate stderr, are cached only for that refresh. Unexpected commands discard the cache and use Majutsu’s normal responsive runner. Mutations, editors and signing are not batched.

Batching requires Majutsu’s in-memory stderr-buffer support; without it, refreshes run unchanged. Custom repository, operation or log-specific configuration arguments bypass batching. Set tramp-rpc-majutsu-optimize to nil to disable the integration. Workspace-root lookups are also memoized for the duration of opening a log buffer, not across commands.

Advice Module (tramp-rpc-advice.el)

Centralizes all advice functions:

  • Process I/O: process-send-string, process-send-region, process-send-eof
  • Process info: signal-process, process-status, process-exit-status
  • Process metadata: process-command, process-tty-name
  • VC integration: vc-call-backend for proper default-directory handling
  • Eglot integration: Bypass shell wrapping for RPC connections

RPC Server Methods

The dispatcher exposes these public methods:

CategoryMethodDescription
BatchbatchExecute up to 64 requests with bounded concurrency.
Filefile.statReturn file metadata.
Filefile.accessCheck effective-credential access; return errno (0 on success) and a failure message.
Filefile.truenameResolve a file’s canonical path.
Directorydir.listList directory entries, optionally with attributes.
Directorydir.createCreate a directory.
Directorydir.removeRemove a directory.
File I/Ofile.readRead file bytes.
File I/Ofile.writeWrite file bytes.
File I/Ofile.copyCopy a file.
File I/Ofile.renameRename a file.
File I/Ofile.deleteDelete a file.
File I/Ofile.set_modesSet file mode bits.
File I/Ofile.set_timesSet file timestamps.
File I/Ofile.make_symlinkCreate a symbolic link.
File I/Ofile.make_hardlinkCreate a hard link.
File I/Ofile.chownChange file ownership.
Processprocess.runRun a command synchronously.
Processprocess.startStart a managed pipe process; its output is pushed.
Processprocess.writeWrite to managed process stdin.
Processprocess.close_stdinClose managed process stdin.
Processprocess.killSignal a managed process.
Processprocess.signalSignal a positive OS PID permitted by the server’s credentials.
PTYprocess.start_ptyStart a pseudo-terminal process; its output is pushed.
PTYprocess.write_ptyWrite pseudo-terminal input.
PTYprocess.resize_ptyResize a pseudo-terminal.
PTYprocess.kill_ptySignal a pseudo-terminal process.
PTYprocess.close_ptyClose a pseudo-terminal process.
Systemsystem.pingProbe the connection using reserved control capacity.
Systemsystem.infoReturn server and host information.
Systemsystem.statvfsReturn filesystem capacity information.
Commandscommands.run_parallelRun commands concurrently.
Commandsancestors.scanScan ancestor directories.
High-levelhighlevel.test_files_in_dirFind named files in a directory.
High-levelhighlevel.locate_dominating_file_multiFind an ancestor containing one of several names.
High-levelhighlevel.dir_locals_find_file_cache_updateUpdate directory-local file cache data.
Watchwatch.addAdd a filesystem watch.
Watchwatch.removeRemove a filesystem watch.

The server also pushes notifications, which carry no request id:

  • fs.events reports changes below watched directories.
  • process.output carries stdout and stderr bytes of a started process (PTY output arrives as stdout), and process.exit its exit_code plus the terminating signal, if any. A process only starts pushing after its start response has been written.

Binary Deployment

How It Works

User connects via /rpc:host:/path
         │
         ▼
Remote already has binary? ──yes──► Done
         │ no
         ▼
Check local cache (~/.emacs.d/tramp-rpc/VERSION/ARCH/)
         │
         ├─ Found ──────────────────► Transfer to remote
         │
         ▼
Download from GitHub Releases
         │
         ├─ Success ────────────────► Cache locally, transfer to remote
         │
         ▼
Build with cargo (if Rust installed)
         │
         ├─ Success ────────────────► Cache locally, transfer to remote
         │
         ▼
Error with helpful instructions

Supported Platforms

PlatformArchitectureStatus
Linuxx86_64✓
Linuxaarch64✓
Linuxi686✓
Linuxarmv7✓
Linuxarmv5te✓
Linuxarm/ARMv6✓
macOSx86_64✓
macOS (Apple Silicon)aarch64✓

Manual Binary Installation

If automatic deployment fails, download manually from GitHub Releases, extract the archive, and place the binary on the remote host at:

~/.cache/emacs/tramp-rpc/tramp-rpc-server-VERSION

For example:

~/.cache/emacs/tramp-rpc/tramp-rpc-server-0.9.0

Building from Source

Using Nix (recommended)

# Build for current platform
nix build

# Cross-compile for specific target
nix build .#tramp-rpc-server-x86_64-linux
nix build .#tramp-rpc-server-aarch64-linux

# Development shell with all tools
nix develop

Using Cargo

cd server
cargo build --release

The binary will be at target/release/tramp-rpc-server.

Cross-compilation with Cargo

# Install target
rustup target add aarch64-unknown-linux-gnu

# Build (requires appropriate linker)
cargo build --release --target aarch64-unknown-linux-gnu

Configuration

;; Prefer building from source over downloading for release/package installs
;; (default: nil)
(setq tramp-rpc-deploy-prefer-build t)

;; Git checkout policy (default: auto):
;; - auto: reuse source-keyed artifacts; use
;;   M-x tramp-rpc-deploy-install-binary when one must be obtained;
;;   use C-u M-x tramp-rpc-deploy-install-binary to replace an existing one
;; - build: strictly build from source, using a build-only binary identity
;; - release: use release-version binaries and paths
(setq tramp-rpc-deploy-git-build-policy 'build)

;; Local cache directory (default: ~/.emacs.d/tramp-rpc/)
(setq tramp-rpc-deploy-local-cache-directory "~/.cache/emacs/tramp-rpc-binaries")

;; Remote installation directory (default: ~/.cache/emacs/tramp-rpc)
(setq tramp-rpc-deploy-remote-directory "~/.local/bin/tramp-rpc")

;; Disable automatic deployment (default: t)
(setq tramp-rpc-deploy-auto-deploy nil)

;; Use different GitHub repo for downloads
(setq tramp-rpc-deploy-github-repo "myuser/my-fork")

;; Download timeout in seconds (default: 120)
(setq tramp-rpc-deploy-download-timeout 60)

Deployment fallback policy

When the expected remote binary already exists and is executable, TRAMP-RPC reuses it if no trusted local artifact can be obtained (for example because download, build, or local-cache access is unavailable). This fallback never covers a missing remote binary. Whenever a trusted local artifact is available, TRAMP-RPC compares SHA256 digests and either reuses the match or, with automatic deployment enabled, replaces a mismatch through the verified staging-and-activation operation. With automatic deployment disabled, a verified mismatch remains an explicit error.

At startup, the client requires an exact protocol_revision match from system.info. Older servers without this field are rejected and the partial connection is closed. There is no protocol negotiation or automatic retry: rebuild and redeploy the server matching the installed Elisp. A breaking protocol change must ship with new, consistently versioned release binaries; old release assets cannot be reused just because their executable starts.

File operation compatibility notes

File copies use open descriptors for both data and metadata. No-overwrite creates the destination exclusively with private permissions. Overwrite follows an existing destination symlink, but rejects dangling links, devices, FIFOs, and aliases of the source before truncating. Copy errors leave partial output rather than risk deleting a concurrently replaced pathname.

Non-overwriting renames use the filesystem’s atomic no-replace operation, falling back to an exclusive hard link followed by source removal for non-directories. If source removal fails, the error is reported and the destination is retained: deleting it could remove the last copy or a concurrently replaced file. Filesystems without either operation fail explicitly rather than risk overwriting a concurrently created destination.

Process compatibility notes

  • interrupt-process on an RPC PTY honors CURRENT-GROUP: non-nil targets the terminal’s foreground job; lambda does nothing when the shell owns the terminal. Signaling a foreground job does not retire the shell or discard its output.
  • RPC PTYs disable kernel ECHO, ECHONL, and ONLCR. Emacs terminal consumers such as comint and eat perform local echo and line handling; code that relies directly on kernel echo or CRLF conversion will observe different PTY semantics.
  • RPC PTY writes wait for the remote write acknowledgement and can therefore block for up to the RPC timeout when the remote program stops reading. Pipe writes remain queued by default; set tramp-rpc-synchronous-pipe-writes non-nil to make them synchronous too.
  • C-g while waiting for a synchronous, batch, or pipelined RPC response abandons only that wait. The connection, its other pending requests, and its remote processes stay alive, and a late reply for the abandoned request is discarded by its ID. The remote operation itself may still run to completion. A quit inside a request write still retires the connection because the frame may be incomplete, and RPC timeouts and transport failures keep tearing the connection down.

Troubleshooting

Check deployment status

Run M-x tramp-rpc-deploy-status to see:

  • Current version
  • Local architecture
  • Whether Rust/cargo is available
  • Cached binaries
  • Download URLs
  • The binary ID (in case you are manually installing the binary on the remote out of band)

diff-hl issues in dired

If you experience issues with diff-hl in dired buffers on remote hosts:

(setq diff-hl-disable-on-remote t)

Connection issues

The server binary is deployed using standard SSH (scpx method by default). Ensure you can connect to the remote host with:

ssh -o BatchMode=yes user@host echo success

Download failures

If GitHub downloads fail (corporate firewall, etc.), you can:

  1. Install Rust 1.88 or later and let tramp-rpc build locally:
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
        
  2. Download manually and place in the cache directory (see above)
  3. Pre-deploy to remote hosts using your own method

Protocol

TRAMP-RPC uses MessagePack-RPC over stdin/stdout with length-prefixed binary framing.

Protocol History

TRAMP-RPC originally used JSON-RPC with newline-delimited messages. This was changed to MessagePack-RPC for several reasons:

AspectJSON-RPC (old)MessagePack-RPC (current)
Binary dataBase64 encoded (~33% overhead)Native binary type (no overhead)
Message framingNewline-delimitedLength-prefixed binary
Non-UTF8 filenamesRequired escaping/encodingNative binary support
Boolean falseJSON falseMessagePack false (0xc2)
Message sizeLarger (text format)~33% smaller (binary format)

The switch eliminates encoding overhead for file transfers and fixes edge cases with non-UTF8 filenames that are valid on Unix filesystems.

Why MessagePack?

MessagePack is a binary serialization format that provides:

  • Native binary data support (no base64 encoding needed for file content)
  • ~33% smaller messages compared to JSON
  • Faster serialization/deserialization
  • Proper distinction between null and false values

Framing

Each message is prefixed with a 4-byte big-endian length:

<4-byte length><MessagePack payload>

Message Format

Request (conceptual structure):

((version . "2.0")
 (id . 1)
 (method . "file.stat")
 (params . ((path . "/etc/passwd"))))

Response (conceptual structure):

((version . "2.0")
 (id . 1)
 (result . ((type . "file")
            (size . 2847)
            (mode . 420))))

Binary Data

File content, paths, and process I/O are transmitted as raw binary (MessagePack bin type), eliminating encoding overhead and ensuring correct handling of:

  • Non-UTF8 filenames
  • Binary file content
  • Arbitrary byte sequences in process output

Performance

TRAMP-RPC significantly outperforms traditional TRAMP for most operations:

OperationRPC (median)SSH (median)Speedup
connection-setup31 ms1.17 s38.2x
file-exists3.3 ms38.8 ms11.9x
file-attributes3.4 ms23.3 ms6.8x
dir-files-and-attrs3.5 ms95.9 ms27.1x
file-read7.7 ms20.2 ms2.6x
file-write74.2 ms219.9 ms3.0x
directory-files12.1 ms37.2 ms3.1x
copy-file43.1 ms192.0 ms4.5x
10x file-attributes37.6 ms304.7 ms8.1x

Batch operations provide additional 2-4x speedup by combining multiple requests into a single round-trip (e.g., 10x file.stat drops from 37.6 ms sequential to 9.1 ms batched).

For detailed benchmarks and an in-depth technical comparison with original TRAMP, see Technical Comparison.

Testing

TRAMP-RPC includes a comprehensive test suite using Emacs ERT (Emacs Lisp Regression Testing).

Test Categories

CategoryTestsRequirements
Protocol8None (pure Elisp)
Conversion2None (pure Elisp)
Server Integration4RPC server binary
Multi-hop21None (pure Elisp)
Autoload8None (pure Elisp)
Remote File Ops53SSH + RPC server

Running Tests

Quick Protocol Tests (no dependencies)

./test/run-tests.sh --protocol

Or directly with Emacs:

emacs -Q --batch -l test/tramp-rpc-mock-tests.el \
  --eval "(ert-run-tests-batch-and-exit \"^tramp-rpc-mock-test-protocol\")"

All Mock Tests (includes server integration)

./test/run-tests.sh --mock

This runs protocol tests plus server integration tests that communicate directly with the RPC server (no SSH needed).

Kernel Access and ACL Tests

Compare native Emacs access predicates and access-file against an isolated server copy (Linux or macOS):

TRAMP_RPC_ACCESS_TEST_SERVER=/path/to/isolated/tramp-rpc-server \
  emacs -Q --batch -L lisp -l test/run-access-tests.el

On Linux, also test real non-owner named-user/group ACL grants, ACL masks, directory search, missing-target writability, and cache invalidation after ACL changes:

TRAMP_RPC_ACCESS_TEST_SERVER=/path/to/rebuilt/tramp-rpc-server \
  test/run-access-acl-tests.sh

The ACL runner copies the binary and uses a disposable user namespace with DAC bypass capabilities removed from both Emacs and the server. It requires an ACL-capable filesystem, subordinate UID/GID ranges, unshare, newuidmap, newgidmap, setpriv, and setfacl. TRAMP_SOURCE and MSGPACK_SOURCE can supply dependency source directories; otherwise install TRAMP and msgpack as Emacs packages. Both runners fail on missing prerequisites or skipped tests. The regular mock suite skips the ACL test outside this isolated setup.

Full Remote Tests (requires SSH)

TRAMP_RPC_TEST_HOST=your-remote-host ./test/run-tests.sh --remote

Or:

emacs -Q --batch \
  -l test/tramp-rpc-tests.el \
  --eval "(setq tramp-rpc-test-host \"your-remote-host\")" \
  --eval "(ert-run-tests-batch-and-exit \"^tramp-rpc-test\")"

Test Files

  • test/tramp-rpc-tests.el - Full ERT test suite for remote operations
  • test/tramp-rpc-mock-tests.el - CI-compatible tests (no SSH required)
  • test/tramp-rpc-autoload-tests.el - Autoload mechanism tests
  • test/run-tramp-tests.el - Upstream tramp-tests.el against tramp-rpc backend
  • test/run-tests.sh - Test runner script
  • test/run-autoload-tests.sh - Autoload test runner

CI Integration

The GitHub Actions workflow runs:

  1. Rust Build - Builds for 4 targets (x86_64/aarch64 Linux/macOS) with format check; macOS jobs also compare the built server’s access behavior with native Emacs, including when the binary is restored from cache
  2. Elisp Byte-compile - Verifies all .el files compile without errors
  3. Autoload Tests - Verifies method registration and handler setup
  4. Protocol Tests - MessagePack-RPC encoding/decoding (no server)
  5. Multi-hop Tests - ProxyJump conversion, connection keys, hop normalization
  6. Server Integration Tests - Direct server communication (Rust binary)
  7. Full Test Suite - Complete tests via SSH to localhost
  8. Upstream TRAMP Tests - Runs tramp-tests.el against tramp-rpc backend

License

This project is licensed under the GNU General Public License v3.0 or later - see the LICENSE file for details.

Contributing

Contributions welcome! Please ensure code passes cargo clippy and cargo test before submitting.

For Emacs Lisp changes, also run the test suite:

./test/run-tests.sh --mock

About

High-performance TRAMP backend using JSON-RPC instead of shell parsing

Resources

Stars

351 stars

Watchers

6 watching

Forks

Releases

Packages

Contributors

Languages