automerge-rb
Ruby bindings for the upstream Automerge Rust core.
Install
gem install automerge-rb
Precompiled native gems are published for the following platforms, so end users do not need Rust or a C toolchain installed:
arm64-darwin(Apple Silicon macOS)x86_64-darwin(Intel macOS)x86_64-linux(glibc)aarch64-linux(glibc)x64-mingw-ucrt(Windows)
Each native gem ships a fat binary covering Ruby 3.0–3.4. RubyGems auto-selects the matching artifact for the host's platform and Ruby ABI at install time.
On any other platform gem install automerge-rb falls back to building from
source, which requires Rust (1.89+) and a C compiler. The source build also
runs whenever you set --platform=ruby explicitly.
Build from source
bundle exec rake compile
The Automerge checkout currently requires Rust 1.89 or newer. If your default toolchain is older, install one with:
rustup toolchain install 1.89.0
The extension builds the Rust core in release mode by default. Use
AUTOMERGE_RB_DEBUG=1 bundle exec rake compile for a faster local debug build.
By default extconf.rb skips the Rust step if a prebuilt
lib/automerge/<ruby-abi>/automerge_ext.<dlext> already exists (i.e. inside a
native gem); set AUTOMERGE_SOURCE_DIR to point at a different Automerge Rust
workspace if you want to recompile from a custom checkout.
Cross-compiling native gems
Maintainers can build the published binary set with rake-compiler-dock:
bundle exec rake gem:native
This invokes the same docker-backed cross toolchain that the release workflow
uses, producing pkg/automerge-rb-<version>-<platform>.gem for every supported
platform.
Test alignment
The default test suite includes Ruby ports of upstream Automerge C/Rust conformance cases and binary fixture checks:
bundle exec rake test
To also run the vendored upstream Rust core test suite, use:
bundle exec rake test:upstream_core
To run the upstream C API suite, install CMake and cmocka, then use:
bundle exec rake test:upstream_c
For a full local gate, run all three:
bundle exec rake conformance
If multiple Rust toolchains are installed, the Rake tasks prefer
AUTOMERGE_RB_RUST_TOOLCHAIN and otherwise use 1.89.0 when present so cargo
and rustc stay on the same toolchain.
Usage
require "automerge"
doc = Automerge.from("cards" => [])
doc.insert(["cards"], 0, "title" => "Rewrite everything in Ruby", "done" => false)
doc.change(message: "Add card")
bytes = doc.save
loaded = Automerge.load(bytes)
loaded.to_h
The Ruby API is path-oriented. A path is an array of map keys and list indexes:
doc.put(["cards", 0, "done"], true)
doc.get(["cards", 0, "done"]) #=> true
Supported Automerge-specific scalar wrappers:
Automerge::Counter.new(1)
Automerge::Timestamp.new(1_735_689_600_000)
Automerge::Bytes.new("\x00\x01".b)
Automerge::Text.new("mutable text")
Automerge::Uint.new(42)
Implemented document operations include:
- map/list/text reads and writes through path arrays
- commits, empty changes, rollback, actor IDs, heads, and forks
- save/load, incremental save/load, change lookup, missing deps, and applying changes
- merge conflict reads with
get_all - the stateful sync protocol with
SyncState - text cursors and text marks
Historical reads
Every read accepts an optional heads: keyword argument naming a vector of
change hashes (typically returned by Document#heads). When supplied, the read
resolves against that historical snapshot instead of the current HEAD:
doc = Automerge.init
doc.put(["k"], "v1"); doc.commit
h1 = doc.heads
doc.put(["k"], "v2"); doc.commit
doc.get(["k"]) #=> "v2"
doc.get(["k"], heads: h1) #=> "v1"
doc.keys([], heads: h1) #=> ["k"]
doc.length(["list"], heads: h1)
doc.cursor_position(["text"], cursor, heads: h1)
doc.marks(["text"], heads: h1)
doc.get_all(["k"], heads: h1)
Change metadata
Automerge.decode_change(bytes) unpacks a change-bytes blob (from
Document#change_by_hash or Document#get_changes) into a hash describing the
change. Useful for audit logs and replication tooling:
hash = doc.commit(message: "Add card", timestamp: Time.now.to_i)
= Automerge.decode_change(doc.change_by_hash(hash))
[:message] # => "Add card"
[:actor_id] # => "deadbeef"
[:timestamp] # => 1735689600
[:seq] # => 4
[:deps] # => [<change hash>, ...]
[:empty] # => false
Thread safety
Automerge::Document instances are not thread-safe. Two threads sharing the
same Document must coordinate externally; concurrent mutations or even
concurrent reads against an in-progress writer will trigger undefined behavior
in the underlying Rust core. Sharing across threads is safe only after a
save/load round-trip or fork produces independent documents.