diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 8519ac5e..4fc75fef 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -2,10 +2,10 @@ name: CI
on:
push:
branches:
- - main
+ - main
pull_request:
branches:
- - main
+ - main
jobs:
fmt:
runs-on: ubuntu-latest
@@ -14,7 +14,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
components: rustfmt
- uses: Swatinem/rust-cache@v1
@@ -28,7 +28,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
components: clippy
- uses: Swatinem/rust-cache@v1
@@ -42,7 +42,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Build rust docs
@@ -51,6 +51,9 @@ jobs:
- name: Install doxygen
run: sudo apt-get install -y doxygen
shell: bash
+ - name: Build C docs
+ run: ./scripts/ci/cmake-docs
+ shell: bash
cargo-deny:
runs-on: ubuntu-latest
@@ -64,50 +67,23 @@ jobs:
- uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
- arguments: '--manifest-path ./rust/Cargo.toml'
command: check ${{ matrix.checks }}
wasm_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - name: Install wasm-bindgen-cli
- run: cargo install wasm-bindgen-cli wasm-opt
- - name: Install wasm32 target
- run: rustup target add wasm32-unknown-unknown
+ - name: Install wasm-pack
+ run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: run tests
run: ./scripts/ci/wasm_tests
- deno_tests:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: denoland/setup-deno@v1
- with:
- deno-version: v1.x
- - name: Install wasm-bindgen-cli
- run: cargo install wasm-bindgen-cli wasm-opt
- - name: Install wasm32 target
- run: rustup target add wasm32-unknown-unknown
- - name: run tests
- run: ./scripts/ci/deno_tests
-
- js_fmt:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: install
- run: yarn global add prettier
- - name: format
- run: prettier -c javascript/.prettierrc javascript
js_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - name: Install wasm-bindgen-cli
- run: cargo install wasm-bindgen-cli wasm-opt
- - name: Install wasm32 target
- run: rustup target add wasm32-unknown-unknown
+ - name: Install wasm-pack
+ run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: run tests
run: ./scripts/ci/js_tests
@@ -118,7 +94,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: nightly-2023-01-26
+ toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Install CMocka
@@ -127,8 +103,6 @@ jobs:
uses: jwlawson/actions-setup-cmake@v1.12
with:
cmake-version: latest
- - name: Install rust-src
- run: rustup component add rust-src
- name: Build and test C bindings
run: ./scripts/ci/cmake-build Release Static
shell: bash
@@ -138,7 +112,9 @@ jobs:
strategy:
matrix:
toolchain:
- - 1.67.0
+ - 1.60.0
+ - nightly
+ continue-on-error: ${{ matrix.toolchain == 'nightly' }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@@ -157,7 +133,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
@@ -170,7 +146,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index b501d526..1f682628 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -30,16 +30,28 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clean
- args: --manifest-path ./rust/Cargo.toml --doc
+ args: --doc
- name: Build Rust docs
uses: actions-rs/cargo@v1
with:
command: doc
- args: --manifest-path ./rust/Cargo.toml --workspace --all-features --no-deps
+ args: --workspace --all-features --no-deps
- name: Move Rust docs
- run: mkdir -p docs && mv rust/target/doc/* docs/.
+ run: mkdir -p docs && mv target/doc/* docs/.
+ shell: bash
+
+ - name: Install doxygen
+ run: sudo apt-get install -y doxygen
+ shell: bash
+
+ - name: Build C docs
+ run: ./scripts/ci/cmake-docs
+ shell: bash
+
+ - name: Move C docs
+ run: mkdir -p docs/automerge-c && mv automerge-c/build/src/html/* docs/automerge-c/.
shell: bash
- name: Configure root page
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
deleted file mode 100644
index 762671ff..00000000
--- a/.github/workflows/release.yaml
+++ /dev/null
@@ -1,214 +0,0 @@
-name: Release
-on:
- push:
- branches:
- - main
-
-jobs:
- check_if_wasm_version_upgraded:
- name: Check if WASM version has been upgraded
- runs-on: ubuntu-latest
- outputs:
- wasm_version: ${{ steps.version-updated.outputs.current-package-version }}
- wasm_has_updated: ${{ steps.version-updated.outputs.has-updated }}
- steps:
- - uses: JiPaix/package-json-updated-action@v1.0.5
- id: version-updated
- with:
- path: rust/automerge-wasm/package.json
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- publish-wasm:
- name: Publish WASM package
- runs-on: ubuntu-latest
- needs:
- - check_if_wasm_version_upgraded
- # We create release only if the version in the package.json has been upgraded
- if: needs.check_if_wasm_version_upgraded.outputs.wasm_has_updated == 'true'
- steps:
- - uses: actions/setup-node@v3
- with:
- node-version: '16.x'
- registry-url: 'https://registry.npmjs.org'
- - uses: denoland/setup-deno@v1
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
- ref: ${{ github.ref }}
- - name: Get rid of local github workflows
- run: rm -r .github/workflows
- - name: Remove tmp_branch if it exists
- run: git push origin :tmp_branch || true
- - run: git checkout -b tmp_branch
- - name: Install wasm-bindgen-cli
- run: cargo install wasm-bindgen-cli wasm-opt
- - name: Install wasm32 target
- run: rustup target add wasm32-unknown-unknown
- - name: run wasm js tests
- id: wasm_js_tests
- run: ./scripts/ci/wasm_tests
- - name: run wasm deno tests
- id: wasm_deno_tests
- run: ./scripts/ci/deno_tests
- - name: build release
- id: build_release
- run: |
- npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm run release
- - name: Collate deno release files
- if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
- run: |
- mkdir $GITHUB_WORKSPACE/deno_wasm_dist
- cp $GITHUB_WORKSPACE/rust/automerge-wasm/deno/* $GITHUB_WORKSPACE/deno_wasm_dist
- cp $GITHUB_WORKSPACE/rust/automerge-wasm/index.d.ts $GITHUB_WORKSPACE/deno_wasm_dist
- cp $GITHUB_WORKSPACE/rust/automerge-wasm/README.md $GITHUB_WORKSPACE/deno_wasm_dist
- cp $GITHUB_WORKSPACE/rust/automerge-wasm/LICENSE $GITHUB_WORKSPACE/deno_wasm_dist
- sed -i '1i /// ' $GITHUB_WORKSPACE/deno_wasm_dist/automerge_wasm.js
- - name: Create npm release
- if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
- run: |
- if [ "$(npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm show . version)" = "$VERSION" ]; then
- echo "This version is already published"
- exit 0
- fi
- EXTRA_ARGS="--access public"
- if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
- echo "Is pre-release version"
- EXTRA_ARGS="$EXTRA_ARGS --tag next"
- fi
- if [ "$NODE_AUTH_TOKEN" = "" ]; then
- echo "Can't publish on NPM, You need a NPM_TOKEN secret."
- false
- fi
- npm publish $GITHUB_WORKSPACE/rust/automerge-wasm $EXTRA_ARGS
- env:
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- - name: Commit wasm deno release files
- run: |
- git config --global user.name "actions"
- git config --global user.email actions@github.com
- git add $GITHUB_WORKSPACE/deno_wasm_dist
- git commit -am "Add deno release files"
- git push origin tmp_branch
- - name: Tag wasm release
- if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
- uses: softprops/action-gh-release@v1
- with:
- name: Automerge Wasm v${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- tag_name: js/automerge-wasm-${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- target_commitish: tmp_branch
- generate_release_notes: false
- draft: false
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Remove tmp_branch
- run: git push origin :tmp_branch
- check_if_js_version_upgraded:
- name: Check if JS version has been upgraded
- runs-on: ubuntu-latest
- outputs:
- js_version: ${{ steps.version-updated.outputs.current-package-version }}
- js_has_updated: ${{ steps.version-updated.outputs.has-updated }}
- steps:
- - uses: JiPaix/package-json-updated-action@v1.0.5
- id: version-updated
- with:
- path: javascript/package.json
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- publish-js:
- name: Publish JS package
- runs-on: ubuntu-latest
- needs:
- - check_if_js_version_upgraded
- - check_if_wasm_version_upgraded
- - publish-wasm
- # We create release only if the version in the package.json has been upgraded and after the WASM release
- if: |
- (always() && ! cancelled()) &&
- (needs.publish-wasm.result == 'success' || needs.publish-wasm.result == 'skipped') &&
- needs.check_if_js_version_upgraded.outputs.js_has_updated == 'true'
- steps:
- - uses: actions/setup-node@v3
- with:
- node-version: '16.x'
- registry-url: 'https://registry.npmjs.org'
- - uses: denoland/setup-deno@v1
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
- ref: ${{ github.ref }}
- - name: Get rid of local github workflows
- run: rm -r .github/workflows
- - name: Remove js_tmp_branch if it exists
- run: git push origin :js_tmp_branch || true
- - run: git checkout -b js_tmp_branch
- - name: check js formatting
- run: |
- yarn global add prettier
- prettier -c javascript/.prettierrc javascript
- - name: run js tests
- id: js_tests
- run: |
- cargo install wasm-bindgen-cli wasm-opt
- rustup target add wasm32-unknown-unknown
- ./scripts/ci/js_tests
- - name: build js release
- id: build_release
- run: |
- npm --prefix $GITHUB_WORKSPACE/javascript run build
- - name: build js deno release
- id: build_deno_release
- run: |
- VERSION=$WASM_VERSION npm --prefix $GITHUB_WORKSPACE/javascript run deno:build
- env:
- WASM_VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- - name: run deno tests
- id: deno_tests
- run: |
- npm --prefix $GITHUB_WORKSPACE/javascript run deno:test
- - name: Collate deno release files
- if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
- run: |
- mkdir $GITHUB_WORKSPACE/deno_js_dist
- cp $GITHUB_WORKSPACE/javascript/deno_dist/* $GITHUB_WORKSPACE/deno_js_dist
- - name: Create npm release
- if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
- run: |
- if [ "$(npm --prefix $GITHUB_WORKSPACE/javascript show . version)" = "$VERSION" ]; then
- echo "This version is already published"
- exit 0
- fi
- EXTRA_ARGS="--access public"
- if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
- echo "Is pre-release version"
- EXTRA_ARGS="$EXTRA_ARGS --tag next"
- fi
- if [ "$NODE_AUTH_TOKEN" = "" ]; then
- echo "Can't publish on NPM, You need a NPM_TOKEN secret."
- false
- fi
- npm publish $GITHUB_WORKSPACE/javascript $EXTRA_ARGS
- env:
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- VERSION: ${{ needs.check_if_js_version_upgraded.outputs.js_version }}
- - name: Commit js deno release files
- run: |
- git config --global user.name "actions"
- git config --global user.email actions@github.com
- git add $GITHUB_WORKSPACE/deno_js_dist
- git commit -am "Add deno js release files"
- git push origin js_tmp_branch
- - name: Tag JS release
- if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
- uses: softprops/action-gh-release@v1
- with:
- name: Automerge v${{ needs.check_if_js_version_upgraded.outputs.js_version }}
- tag_name: js/automerge-${{ needs.check_if_js_version_upgraded.outputs.js_version }}
- target_commitish: js_tmp_branch
- generate_release_notes: false
- draft: false
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Remove js_tmp_branch
- run: git push origin :js_tmp_branch
diff --git a/.gitignore b/.gitignore
index f77865d0..4ca7b595 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
+/target
/.direnv
perf.*
/Cargo.lock
build/
+automerge/proptest-regressions/
.vim/*
-/target
diff --git a/rust/Cargo.toml b/Cargo.toml
similarity index 78%
rename from rust/Cargo.toml
rename to Cargo.toml
index 5d29fc9f..9add8e60 100644
--- a/rust/Cargo.toml
+++ b/Cargo.toml
@@ -3,15 +3,15 @@ members = [
"automerge",
"automerge-c",
"automerge-cli",
- "automerge-test",
"automerge-wasm",
"edit-trace",
]
resolver = "2"
[profile.release]
+debug = true
lto = true
-codegen-units = 1
+opt-level = 3
[profile.bench]
-debug = true
\ No newline at end of file
+debug = true
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..a1f3fd62
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+.PHONY: rust
+rust:
+ cd automerge && cargo test
+
+.PHONY: wasm
+wasm:
+ cd automerge-wasm && yarn
+ cd automerge-wasm && yarn build
+ cd automerge-wasm && yarn test
+ cd automerge-wasm && yarn link
+
+.PHONY: js
+js: wasm
+ cd automerge-js && yarn
+ cd automerge-js && yarn link "automerge-wasm"
+ cd automerge-js && yarn test
+
+.PHONY: clean
+clean:
+ git clean -x -d -f
diff --git a/README.md b/README.md
index ad174da4..64b0f9b7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Automerge
+# Automerge RS
@@ -7,141 +7,103 @@
[](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml)
[](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml)
-Automerge is a library which provides fast implementations of several different
-CRDTs, a compact compression format for these CRDTs, and a sync protocol for
-efficiently transmitting those changes over the network. The objective of the
-project is to support [local-first](https://www.inkandswitch.com/local-first/) applications in the same way that relational
-databases support server applications - by providing mechanisms for persistence
-which allow application developers to avoid thinking about hard distributed
-computing problems. Automerge aims to be PostgreSQL for your local-first app.
+This is a Rust library implementation of the [Automerge](https://github.com/automerge/automerge) file format and network protocol. Its focus is to support the creation of Automerge implementations in other languages, currently; WASM, JS and C. A `libautomerge` if you will.
-If you're looking for documentation on the JavaScript implementation take a look
-at https://automerge.org/docs/hello/. There are other implementations in both
-Rust and C, but they are earlier and don't have documentation yet. You can find
-them in `rust/automerge` and `rust/automerge-c` if you are comfortable
-reading the code and tests to figure out how to use them.
-
-If you're familiar with CRDTs and interested in the design of Automerge in
-particular take a look at https://automerge.org/docs/how-it-works/backend/
-
-Finally, if you want to talk to us about this project please [join the
-Slack](https://join.slack.com/t/automerge/shared_invite/zt-e4p3760n-kKh7r3KRH1YwwNfiZM8ktw)
+The original [Automerge](https://github.com/automerge/automerge) project (written in JS from the ground up) is still very much maintained and recommended. Indeed it is because of the success of that project that the next stage of Automerge is being explored here. Hopefully Rust can offer a more performant and scalable Automerge, opening up even more use cases.
## Status
-This project is formed of a core Rust implementation which is exposed via FFI in
-javascript+WASM, C, and soon other languages. Alex
-([@alexjg](https://github.com/alexjg/)]) is working full time on maintaining
-automerge, other members of Ink and Switch are also contributing time and there
-are several other maintainers. The focus is currently on shipping the new JS
-package. We expect to be iterating the API and adding new features over the next
-six months so there will likely be several major version bumps in all packages
-in that time.
+The project has 5 components:
-In general we try and respect semver.
+1. [_automerge_](automerge) - The main Rust implementation of the library.
+2. [_automerge-wasm_](automerge-wasm) - A JS/WASM interface to the underlying Rust library. This API is generally mature and in use in a handful of projects.
+3. [_automerge-js_](automerge-js) - This is a Javascript library using the WASM interface to export the same public API of the primary Automerge project. Currently this project passes all of Automerge's tests but has not been used in any real project or packaged as an NPM. Alpha testers welcome.
+4. [_automerge-c_](automerge-c) - This is a C library intended to be an FFI integration point for all other languages. It is currently a work in progress and not yet ready for any testing.
+5. [_automerge-cli_](automerge-cli) - An experimental CLI wrapper around the Rust library. Currently not functional.
-### JavaScript
+## How?
-A stable release of the javascript package is currently available as
-`@automerge/automerge@2.0.0` where. pre-release verisions of the `2.0.1` are
-available as `2.0.1-alpha.n`. `2.0.1*` packages are also available for Deno at
-https://deno.land/x/automerge
+The magic of the architecture is built around the `OpTree`. This is a data structure
+which supports efficiently inserting new operations and realising values of
+existing operations. Most interactions with the `OpTree` are in the form of
+implementations of `TreeQuery` - a trait which can be used to traverse the
+`OpTree` and producing state of some kind. User facing operations are exposed on
+an `Automerge` object, under the covers these operations typically instantiate
+some `TreeQuery` and run it over the `OpTree`.
-### Rust
+## Development
-The rust codebase is currently oriented around producing a performant backend
-for the Javascript wrapper and as such the API for Rust code is low level and
-not well documented. We will be returning to this over the next few months but
-for now you will need to be comfortable reading the tests and asking questions
-to figure out how to use it. If you are looking to build rust applications which
-use automerge you may want to look into
-[autosurgeon](https://github.com/alexjg/autosurgeon)
+Please feel free to open issues and pull requests.
-## Repository Organisation
+### Running CI
-- `./rust` - the rust rust implementation and also the Rust components of
- platform specific wrappers (e.g. `automerge-wasm` for the WASM API or
- `automerge-c` for the C FFI bindings)
-- `./javascript` - The javascript library which uses `automerge-wasm`
- internally but presents a more idiomatic javascript interface
-- `./scripts` - scripts which are useful to maintenance of the repository.
- This includes the scripts which are run in CI.
-- `./img` - static assets for use in `.md` files
+The steps CI will run are all defined in `./scripts/ci`. Obviously CI will run
+everything when you submit a PR, but if you want to run everything locally
+before you push you can run `./scripts/ci/run` to run everything.
-## Building
+### Running the JS tests
-To build this codebase you will need:
+You will need to have [node](https://nodejs.org/en/), [yarn](https://yarnpkg.com/getting-started/install), [rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) installed.
-- `rust`
-- `node`
-- `yarn`
-- `cmake`
-- `cmocka`
+To build and test the rust library:
-You will also need to install the following with `cargo install`
-
-- `wasm-bindgen-cli`
-- `wasm-opt`
-- `cargo-deny`
-
-And ensure you have added the `wasm32-unknown-unknown` target for rust cross-compilation.
-
-The various subprojects (the rust code, the wrapper projects) have their own
-build instructions, but to run the tests that will be run in CI you can run
-`./scripts/ci/run`.
-
-### For macOS
-
-These instructions worked to build locally on macOS 13.1 (arm64) as of
-Nov 29th 2022.
-
-```bash
-# clone the repo
-git clone https://github.com/automerge/automerge-rs
-cd automerge-rs
-
-# install rustup
-curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-
-# install homebrew
-/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-
-# install cmake, node, cmocka
-brew install cmake node cmocka
-
-# install yarn
-npm install --global yarn
-
-# install javascript dependencies
-yarn --cwd ./javascript
-
-# install rust dependencies
-cargo install wasm-bindgen-cli wasm-opt cargo-deny
-
-# get nightly rust to produce optimized automerge-c builds
-rustup toolchain install nightly
-rustup component add rust-src --toolchain nightly
-
-# add wasm target in addition to current architecture
-rustup target add wasm32-unknown-unknown
-
-# Run ci script
-./scripts/ci/run
+```shell
+ $ cd automerge
+ $ cargo test
```
-If your build fails to find `cmocka.h` you may need to teach it about homebrew's
-installation location:
+To build and test the wasm library:
-```
-export CPATH=/opt/homebrew/include
-export LIBRARY_PATH=/opt/homebrew/lib
-./scripts/ci/run
+```shell
+ ## setup
+ $ cd automerge-wasm
+ $ yarn
+
+ ## building or testing
+ $ yarn build
+ $ yarn test
+
+ ## without this the js library wont automatically use changes
+ $ yarn link
+
+ ## cutting a release or doing benchmarking
+ $ yarn release
```
-## Contributing
+To test the js library. This is where most of the tests reside.
-Please try and split your changes up into relatively independent commits which
-change one subsystem at a time and add good commit messages which describe what
-the change is and why you're making it (err on the side of longer commit
-messages). `git blame` should give future maintainers a good idea of why
-something is the way it is.
+```shell
+ ## setup
+ $ cd automerge-js
+ $ yarn
+ $ yarn link "automerge-wasm"
+
+ ## testing
+ $ yarn test
+```
+
+And finally, to build and test the C bindings with CMake:
+
+```shell
+## setup
+$ cd automerge-c
+$ mkdir -p build
+$ cd build
+$ cmake -S .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF
+## building and testing
+$ cmake --build . --target test_automerge
+```
+
+To add debugging symbols, replace `Release` with `Debug`.
+To build a shared library instead of a static one, replace `OFF` with `ON`.
+
+The C bindings can be built and tested on any platform for which CMake is
+available but the steps for doing so vary across platforms and are too numerous
+to list here.
+
+## Benchmarking
+
+The [`edit-trace`](edit-trace) folder has the main code for running the edit trace benchmarking.
+
+## The old Rust project
+If you are looking for the origional `automerge-rs` project that can be used as a wasm backend to the javascript implementation, it can be found [here](https://github.com/automerge/automerge-rs/tree/automerge-1.0).
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 00000000..646c0c20
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,32 @@
+### next steps:
+ 1. C API
+ 2. port rust command line tool
+ 3. fast load
+
+### ergonomics:
+ 1. value() -> () or something that into's a value
+
+### automerge:
+ 1. single pass (fast) load
+ 2. micro-patches / bare bones observation API / fully hydrated documents
+
+### future:
+ 1. handle columns with unknown data in and out
+ 2. branches with different indexes
+
+### Peritext
+ 1. add mark / remove mark -- type, start/end elemid (inclusive,exclusive)
+ 2. track any formatting ops that start or end on a character
+ 3. ops right before the character, ops right after that character
+ 4. query a single character - character, plus marks that start or end on that character
+ what is its current formatting,
+ what are the ops that include that in their span,
+ None = same as last time, Set( bold, italic ),
+ keep these on index
+ 5. op probably belongs with the start character - possible packed at the beginning or end of the list
+
+### maybe:
+ 1. tables
+
+### no:
+ 1. cursors
diff --git a/automerge-c/.gitignore b/automerge-c/.gitignore
new file mode 100644
index 00000000..cb544af0
--- /dev/null
+++ b/automerge-c/.gitignore
@@ -0,0 +1,3 @@
+automerge
+automerge.h
+automerge.o
diff --git a/automerge-c/CMakeLists.txt b/automerge-c/CMakeLists.txt
new file mode 100644
index 00000000..e5a7b1ca
--- /dev/null
+++ b/automerge-c/CMakeLists.txt
@@ -0,0 +1,141 @@
+cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
+
+set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
+
+# Parse the library name, project name and project version out of Cargo's TOML file.
+set(CARGO_LIB_SECTION OFF)
+
+set(LIBRARY_NAME "")
+
+set(CARGO_PKG_SECTION OFF)
+
+set(CARGO_PKG_NAME "")
+
+set(CARGO_PKG_VERSION "")
+
+file(READ Cargo.toml TOML_STRING)
+
+string(REPLACE ";" "\\\\;" TOML_STRING "${TOML_STRING}")
+
+string(REPLACE "\n" ";" TOML_LINES "${TOML_STRING}")
+
+foreach(TOML_LINE IN ITEMS ${TOML_LINES})
+ string(REGEX MATCH "^\\[(lib|package)\\]$" _ ${TOML_LINE})
+
+ if(CMAKE_MATCH_1 STREQUAL "lib")
+ set(CARGO_LIB_SECTION ON)
+
+ set(CARGO_PKG_SECTION OFF)
+ elseif(CMAKE_MATCH_1 STREQUAL "package")
+ set(CARGO_LIB_SECTION OFF)
+
+ set(CARGO_PKG_SECTION ON)
+ endif()
+
+ string(REGEX MATCH "^name += +\"([^\"]+)\"$" _ ${TOML_LINE})
+
+ if(CMAKE_MATCH_1 AND (CARGO_LIB_SECTION AND NOT CARGO_PKG_SECTION))
+ set(LIBRARY_NAME "${CMAKE_MATCH_1}")
+ elseif(CMAKE_MATCH_1 AND (NOT CARGO_LIB_SECTION AND CARGO_PKG_SECTION))
+ set(CARGO_PKG_NAME "${CMAKE_MATCH_1}")
+ endif()
+
+ string(REGEX MATCH "^version += +\"([^\"]+)\"$" _ ${TOML_LINE})
+
+ if(CMAKE_MATCH_1 AND CARGO_PKG_SECTION)
+ set(CARGO_PKG_VERSION "${CMAKE_MATCH_1}")
+ endif()
+
+ if(LIBRARY_NAME AND (CARGO_PKG_NAME AND CARGO_PKG_VERSION))
+ break()
+ endif()
+endforeach()
+
+project(${CARGO_PKG_NAME} VERSION ${CARGO_PKG_VERSION} LANGUAGES C DESCRIPTION "C bindings for the Automerge Rust backend.")
+
+include(CTest)
+
+option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
+
+include(CMakePackageConfigHelpers)
+
+include(GNUInstallDirs)
+
+string(MAKE_C_IDENTIFIER ${PROJECT_NAME} SYMBOL_PREFIX)
+
+string(TOUPPER ${SYMBOL_PREFIX} SYMBOL_PREFIX)
+
+set(CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Cargo/target")
+
+set(CBINDGEN_INCLUDEDIR "${CARGO_TARGET_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
+
+set(CBINDGEN_TARGET_DIR "${CBINDGEN_INCLUDEDIR}/${PROJECT_NAME}")
+
+add_subdirectory(src)
+
+# Generate and install the configuration header.
+math(EXPR INTEGER_PROJECT_VERSION_MAJOR "${PROJECT_VERSION_MAJOR} * 100000")
+
+math(EXPR INTEGER_PROJECT_VERSION_MINOR "${PROJECT_VERSION_MINOR} * 100")
+
+math(EXPR INTEGER_PROJECT_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
+
+math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + ${INTEGER_PROJECT_VERSION_MINOR} + ${INTEGER_PROJECT_VERSION_PATCH}")
+
+configure_file(
+ ${CMAKE_MODULE_PATH}/config.h.in
+ config.h
+ @ONLY
+ NEWLINE_STYLE LF
+)
+
+install(
+ FILES ${CMAKE_BINARY_DIR}/config.h
+ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
+)
+
+if(BUILD_TESTING)
+ add_subdirectory(test EXCLUDE_FROM_ALL)
+
+ enable_testing()
+endif()
+
+add_subdirectory(examples EXCLUDE_FROM_ALL)
+
+# Generate and install .cmake files
+set(PROJECT_CONFIG_NAME "${PROJECT_NAME}-config")
+
+set(PROJECT_CONFIG_VERSION_NAME "${PROJECT_CONFIG_NAME}-version")
+
+write_basic_package_version_file(
+ ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
+ VERSION ${PROJECT_VERSION}
+ COMPATIBILITY ExactVersion
+)
+
+# The namespace label starts with the title-cased library name.
+string(SUBSTRING ${LIBRARY_NAME} 0 1 NS_FIRST)
+
+string(SUBSTRING ${LIBRARY_NAME} 1 -1 NS_REST)
+
+string(TOUPPER ${NS_FIRST} NS_FIRST)
+
+string(TOLOWER ${NS_REST} NS_REST)
+
+string(CONCAT NAMESPACE ${NS_FIRST} ${NS_REST} "::")
+
+# \note CMake doesn't automate the exporting of an imported library's targets
+# so the package configuration script must do it.
+configure_package_config_file(
+ ${CMAKE_MODULE_PATH}/${PROJECT_CONFIG_NAME}.cmake.in
+ ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
+ INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
+)
+
+install(
+ FILES
+ ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
+ ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
+ DESTINATION
+ ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
+)
diff --git a/rust/automerge-c/Cargo.toml b/automerge-c/Cargo.toml
similarity index 82%
rename from rust/automerge-c/Cargo.toml
rename to automerge-c/Cargo.toml
index 95a3a29c..851a3470 100644
--- a/rust/automerge-c/Cargo.toml
+++ b/automerge-c/Cargo.toml
@@ -7,8 +7,8 @@ license = "MIT"
rust-version = "1.57.0"
[lib]
-name = "automerge_core"
-crate-type = ["staticlib"]
+name = "automerge"
+crate-type = ["cdylib", "staticlib"]
bench = false
doc = false
@@ -19,4 +19,4 @@ libc = "^0.2"
smol_str = "^0.1.21"
[build-dependencies]
-cbindgen = "^0.24"
+cbindgen = "^0.20"
diff --git a/automerge-c/README.md b/automerge-c/README.md
new file mode 100644
index 00000000..1b0e618d
--- /dev/null
+++ b/automerge-c/README.md
@@ -0,0 +1,97 @@
+
+## Methods we need to support
+
+### Basic management
+
+ 1. `AMcreate()`
+ 1. `AMclone(doc)`
+ 1. `AMfree(doc)`
+ 1. `AMconfig(doc, key, val)` // set actor
+ 1. `actor = get_actor(doc)`
+
+### Transactions
+
+ 1. `AMpendingOps(doc)`
+ 1. `AMcommit(doc, message, time)`
+ 1. `AMrollback(doc)`
+
+### Write
+
+ 1. `AMset{Map|List}(doc, obj, prop, value)`
+ 1. `AMinsert(doc, obj, index, value)`
+ 1. `AMpush(doc, obj, value)`
+ 1. `AMdel{Map|List}(doc, obj, prop)`
+ 1. `AMinc{Map|List}(doc, obj, prop, value)`
+ 1. `AMspliceText(doc, obj, start, num_del, text)`
+
+### Read (the heads argument is optional and can be on an `at` variant)
+
+ 1. `AMkeys(doc, obj, heads)`
+ 1. `AMlength(doc, obj, heads)`
+ 1. `AMlistRange(doc, obj, heads)`
+ 1. `AMmapRange(doc, obj, heads)`
+ 1. `AMvalues(doc, obj, heads)`
+ 1. `AMtext(doc, obj, heads)`
+
+### Sync
+
+ 1. `AMgenerateSyncMessage(doc, state)`
+ 1. `AMreceiveSyncMessage(doc, state, message)`
+ 1. `AMinitSyncState()`
+
+### Save / Load
+
+ 1. `AMload(data)`
+ 1. `AMloadIncremental(doc, data)`
+ 1. `AMsave(doc)`
+ 1. `AMsaveIncremental(doc)`
+
+### Low Level Access
+
+ 1. `AMapplyChanges(doc, changes)`
+ 1. `AMgetChanges(doc, deps)`
+ 1. `AMgetChangesAdded(doc1, doc2)`
+ 1. `AMgetHeads(doc)`
+ 1. `AMgetLastLocalChange(doc)`
+ 1. `AMgetMissingDeps(doc, heads)`
+
+### Encode/Decode
+
+ 1. `AMencodeChange(change)`
+ 1. `AMdecodeChange(change)`
+ 1. `AMencodeSyncMessage(change)`
+ 1. `AMdecodeSyncMessage(change)`
+ 1. `AMencodeSyncState(change)`
+ 1. `AMdecodeSyncState(change)`
+
+## Open Question - Memory management
+
+Most of these calls return one or more items of arbitrary length. Doing memory management in C is tricky. This is my proposed solution...
+
+###
+
+ ```
+ // returns 1 or zero opids
+ n = automerge_set(doc, "_root", "hello", datatype, value);
+ if (n) {
+ automerge_pop(doc, &obj, len);
+ }
+
+ // returns n values
+ n = automerge_values(doc, "_root", "hello");
+ for (i = 0; i"
)
-target_link_libraries(${LIBRARY_NAME}_quickstart PRIVATE ${LIBRARY_NAME})
+target_link_libraries(example_quickstart PRIVATE ${LIBRARY_NAME})
-add_dependencies(${LIBRARY_NAME}_quickstart ${BINDINGS_NAME}_artifacts)
+add_dependencies(example_quickstart ${LIBRARY_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
- TARGET ${LIBRARY_NAME}_quickstart
+ TARGET example_quickstart
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
- ${CMAKE_BINARY_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Copying the DLL built by Cargo into the examples directory..."
VERBATIM
)
endif()
add_custom_command(
- TARGET ${LIBRARY_NAME}_quickstart
+ TARGET example_quickstart
POST_BUILD
COMMAND
- ${LIBRARY_NAME}_quickstart
+ example_quickstart
COMMENT
"Running the example quickstart..."
VERBATIM
diff --git a/rust/automerge-c/examples/README.md b/automerge-c/examples/README.md
similarity index 68%
rename from rust/automerge-c/examples/README.md
rename to automerge-c/examples/README.md
index 17e69412..17aa2227 100644
--- a/rust/automerge-c/examples/README.md
+++ b/automerge-c/examples/README.md
@@ -5,5 +5,5 @@
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
-cmake --build automerge-c/build --target automerge_quickstart
+cmake --build automerge-c/build --target example_quickstart
```
diff --git a/automerge-c/examples/quickstart.c b/automerge-c/examples/quickstart.c
new file mode 100644
index 00000000..0c94a1a2
--- /dev/null
+++ b/automerge-c/examples/quickstart.c
@@ -0,0 +1,146 @@
+#include
+#include
+#include
+
+#include
+
+static void abort_cb(AMresultStack**, uint8_t);
+
+/**
+ * \brief Based on https://automerge.github.io/docs/quickstart
+ */
+int main(int argc, char** argv) {
+ AMresultStack* stack = NULL;
+ AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
+ AMobjId const* const cards = AMpush(&stack,
+ AMmapPutObject(doc1, AM_ROOT, "cards", AM_OBJ_TYPE_LIST),
+ AM_VALUE_OBJ_ID,
+ abort_cb).obj_id;
+ AMobjId const* const card1 = AMpush(&stack,
+ AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
+ AM_VALUE_OBJ_ID,
+ abort_cb).obj_id;
+ AMfree(AMmapPutStr(doc1, card1, "title", "Rewrite everything in Clojure"));
+ AMfree(AMmapPutBool(doc1, card1, "done", false));
+ AMobjId const* const card2 = AMpush(&stack,
+ AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
+ AM_VALUE_OBJ_ID,
+ abort_cb).obj_id;
+ AMfree(AMmapPutStr(doc1, card2, "title", "Rewrite everything in Haskell"));
+ AMfree(AMmapPutBool(doc1, card2, "done", false));
+ AMfree(AMcommit(doc1, "Add card", NULL));
+
+ AMdoc* doc2 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
+ AMfree(AMmerge(doc2, doc1));
+
+ AMbyteSpan const binary = AMpush(&stack, AMsave(doc1), AM_VALUE_BYTES, abort_cb).bytes;
+ doc2 = AMpush(&stack, AMload(binary.src, binary.count), AM_VALUE_DOC, abort_cb).doc;
+
+ AMfree(AMmapPutBool(doc1, card1, "done", true));
+ AMfree(AMcommit(doc1, "Mark card as done", NULL));
+
+ AMfree(AMlistDelete(doc2, cards, 0));
+ AMfree(AMcommit(doc2, "Delete card", NULL));
+
+ AMfree(AMmerge(doc1, doc2));
+
+ AMchanges changes = AMpush(&stack, AMgetChanges(doc1, NULL), AM_VALUE_CHANGES, abort_cb).changes;
+ AMchange const* change = NULL;
+ while ((change = AMchangesNext(&changes, 1)) != NULL) {
+ AMbyteSpan const change_hash = AMchangeHash(change);
+ AMchangeHashes const heads = AMpush(&stack,
+ AMchangeHashesInit(&change_hash, 1),
+ AM_VALUE_CHANGE_HASHES,
+ abort_cb).change_hashes;
+ printf("%s %ld\n", AMchangeMessage(change), AMobjSize(doc1, cards, &heads));
+ }
+ AMfreeStack(&stack);
+}
+
+static char const* discriminant_suffix(AMvalueVariant const);
+
+/**
+ * \brief Prints an error message to `stderr`, deallocates all results in the
+ * given stack and exits.
+ *
+ * \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
+ * \param[in] discriminant An `AMvalueVariant` enum tag.
+ * \pre \p stack` != NULL`.
+ * \post `*stack == NULL`.
+ */
+static void abort_cb(AMresultStack** stack, uint8_t discriminant) {
+ static char buffer[512] = {0};
+
+ char const* suffix = NULL;
+ if (!stack) {
+ suffix = "Stack*";
+ }
+ else if (!*stack) {
+ suffix = "Stack";
+ }
+ else if (!(*stack)->result) {
+ suffix = "";
+ }
+ if (suffix) {
+ fprintf(stderr, "Null `AMresult%s*`.", suffix);
+ AMfreeStack(stack);
+ exit(EXIT_FAILURE);
+ return;
+ }
+ AMstatus const status = AMresultStatus((*stack)->result);
+ switch (status) {
+ case AM_STATUS_ERROR: strcpy(buffer, "Error"); break;
+ case AM_STATUS_INVALID_RESULT: strcpy(buffer, "Invalid result"); break;
+ case AM_STATUS_OK: break;
+ default: sprintf(buffer, "Unknown `AMstatus` tag %d", status);
+ }
+ if (buffer[0]) {
+ fprintf(stderr, "%s; %s.", buffer, AMerrorMessage((*stack)->result));
+ AMfreeStack(stack);
+ exit(EXIT_FAILURE);
+ return;
+ }
+ AMvalue const value = AMresultValue((*stack)->result);
+ fprintf(stderr, "Unexpected tag `AM_VALUE_%s` (%d); expected `AM_VALUE_%s`.",
+ discriminant_suffix(value.tag),
+ value.tag,
+ discriminant_suffix(discriminant));
+ AMfreeStack(stack);
+ exit(EXIT_FAILURE);
+}
+
+/**
+ * \brief Gets the suffix for a discriminant's corresponding string
+ * representation.
+ *
+ * \param[in] discriminant An `AMvalueVariant` enum tag.
+ * \return A UTF-8 string.
+ */
+static char const* discriminant_suffix(AMvalueVariant const discriminant) {
+ char const* suffix = NULL;
+ switch (discriminant) {
+ case AM_VALUE_ACTOR_ID: suffix = "ACTOR_ID"; break;
+ case AM_VALUE_BOOLEAN: suffix = "BOOLEAN"; break;
+ case AM_VALUE_BYTES: suffix = "BYTES"; break;
+ case AM_VALUE_CHANGE_HASHES: suffix = "CHANGE_HASHES"; break;
+ case AM_VALUE_CHANGES: suffix = "CHANGES"; break;
+ case AM_VALUE_COUNTER: suffix = "COUNTER"; break;
+ case AM_VALUE_DOC: suffix = "DOC"; break;
+ case AM_VALUE_F64: suffix = "F64"; break;
+ case AM_VALUE_INT: suffix = "INT"; break;
+ case AM_VALUE_LIST_ITEMS: suffix = "LIST_ITEMS"; break;
+ case AM_VALUE_MAP_ITEMS: suffix = "MAP_ITEMS"; break;
+ case AM_VALUE_NULL: suffix = "NULL"; break;
+ case AM_VALUE_OBJ_ID: suffix = "OBJ_ID"; break;
+ case AM_VALUE_OBJ_ITEMS: suffix = "OBJ_ITEMS"; break;
+ case AM_VALUE_STR: suffix = "STR"; break;
+ case AM_VALUE_STRS: suffix = "STRINGS"; break;
+ case AM_VALUE_SYNC_MESSAGE: suffix = "SYNC_MESSAGE"; break;
+ case AM_VALUE_SYNC_STATE: suffix = "SYNC_STATE"; break;
+ case AM_VALUE_TIMESTAMP: suffix = "TIMESTAMP"; break;
+ case AM_VALUE_UINT: suffix = "UINT"; break;
+ case AM_VALUE_VOID: suffix = "VOID"; break;
+ default: suffix = "...";
+ }
+ return suffix;
+}
diff --git a/rust/automerge-c/docs/img/brandmark.png b/automerge-c/img/brandmark.png
similarity index 100%
rename from rust/automerge-c/docs/img/brandmark.png
rename to automerge-c/img/brandmark.png
diff --git a/automerge-c/src/CMakeLists.txt b/automerge-c/src/CMakeLists.txt
new file mode 100644
index 00000000..e02c0a96
--- /dev/null
+++ b/automerge-c/src/CMakeLists.txt
@@ -0,0 +1,250 @@
+cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
+
+find_program (
+ CARGO_CMD
+ "cargo"
+ PATHS "$ENV{CARGO_HOME}/bin"
+ DOC "The Cargo command"
+)
+
+if(NOT CARGO_CMD)
+ message(FATAL_ERROR "Cargo (Rust package manager) not found! Install it and/or set the CARGO_HOME environment variable.")
+endif()
+
+string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
+
+if(BUILD_TYPE_LOWER STREQUAL debug)
+ set(CARGO_BUILD_TYPE "debug")
+
+ set(CARGO_FLAG "")
+else()
+ set(CARGO_BUILD_TYPE "release")
+
+ set(CARGO_FLAG "--release")
+endif()
+
+set(CARGO_FEATURES "")
+
+set(CARGO_CURRENT_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}")
+
+set(
+ CARGO_OUTPUT
+ ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
+ ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}
+ ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
+)
+
+if(WIN32)
+ # \note The basename of an import library output by Cargo is the filename
+ # of its corresponding shared library.
+ list(APPEND CARGO_OUTPUT ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX})
+endif()
+
+add_custom_command(
+ OUTPUT
+ ${CARGO_OUTPUT}
+ COMMAND
+ # \note cbindgen won't regenerate its output header file after it's
+ # been removed but it will after its configuration file has been
+ # updated.
+ ${CMAKE_COMMAND} -DCONDITION=NOT_EXISTS -P ${CMAKE_SOURCE_DIR}/cmake/file_touch.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
+ COMMAND
+ ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} CBINDGEN_TARGET_DIR=${CBINDGEN_TARGET_DIR} ${CARGO_CMD} build ${CARGO_FLAG} ${CARGO_FEATURES}
+ MAIN_DEPENDENCY
+ lib.rs
+ DEPENDS
+ actor_id.rs
+ byte_span.rs
+ change_hashes.rs
+ change.rs
+ changes.rs
+ doc.rs
+ doc/list.rs
+ doc/list/item.rs
+ doc/list/items.rs
+ doc/map.rs
+ doc/map/item.rs
+ doc/map/items.rs
+ doc/utils.rs
+ obj.rs
+ obj/item.rs
+ obj/items.rs
+ result.rs
+ result_stack.rs
+ strs.rs
+ sync.rs
+ sync/have.rs
+ sync/haves.rs
+ sync/message.rs
+ sync/state.rs
+ ${CMAKE_SOURCE_DIR}/build.rs
+ ${CMAKE_SOURCE_DIR}/Cargo.toml
+ ${CMAKE_SOURCE_DIR}/cbindgen.toml
+ WORKING_DIRECTORY
+ ${CMAKE_SOURCE_DIR}
+ COMMENT
+ "Producing the library artifacts with Cargo..."
+ VERBATIM
+)
+
+add_custom_target(
+ ${LIBRARY_NAME}_artifacts ALL
+ DEPENDS ${CARGO_OUTPUT}
+)
+
+# \note cbindgen's naming behavior isn't fully configurable and it ignores
+# `const fn` calls (https://github.com/eqrion/cbindgen/issues/252).
+add_custom_command(
+ TARGET ${LIBRARY_NAME}_artifacts
+ POST_BUILD
+ COMMAND
+ # Compensate for cbindgen's variant struct naming.
+ ${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+_[^_]+\)_Body -DREPLACE_EXPR=AM\\1 -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
+ COMMAND
+ # Compensate for cbindgen's union tag enum type naming.
+ ${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+\)_Tag -DREPLACE_EXPR=AM\\1Variant -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
+ COMMAND
+ # Compensate for cbindgen's translation of consecutive uppercase letters to "ScreamingSnakeCase".
+ ${CMAKE_COMMAND} -DMATCH_REGEX=A_M\([^_]+\)_ -DREPLACE_EXPR=AM_\\1_ -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
+ COMMAND
+ # Compensate for cbindgen ignoring `std:mem::size_of()` calls.
+ ${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
+ WORKING_DIRECTORY
+ ${CMAKE_SOURCE_DIR}
+ COMMENT
+ "Compensating for cbindgen deficits..."
+ VERBATIM
+)
+
+if(BUILD_SHARED_LIBS)
+ if(WIN32)
+ set(LIBRARY_DESTINATION "${CMAKE_INSTALL_BINDIR}")
+ else()
+ set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
+ endif()
+
+ set(LIBRARY_DEFINE_SYMBOL "${SYMBOL_PREFIX}_EXPORTS")
+
+ # \note The basename of an import library output by Cargo is the filename
+ # of its corresponding shared library.
+ set(LIBRARY_IMPLIB "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}")
+
+ set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}")
+
+ set(LIBRARY_NO_SONAME "${WIN32}")
+
+ set(LIBRARY_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}")
+
+ set(LIBRARY_TYPE "SHARED")
+else()
+ set(LIBRARY_DEFINE_SYMBOL "")
+
+ set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
+
+ set(LIBRARY_IMPLIB "")
+
+ set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}")
+
+ set(LIBRARY_NO_SONAME "TRUE")
+
+ set(LIBRARY_SONAME "")
+
+ set(LIBRARY_TYPE "STATIC")
+endif()
+
+add_library(${LIBRARY_NAME} ${LIBRARY_TYPE} IMPORTED GLOBAL)
+
+set_target_properties(
+ ${LIBRARY_NAME}
+ PROPERTIES
+ # \note Cargo writes a debug build into a nested directory instead of
+ # decorating its name.
+ DEBUG_POSTFIX ""
+ DEFINE_SYMBOL "${LIBRARY_DEFINE_SYMBOL}"
+ IMPORTED_IMPLIB "${LIBRARY_IMPLIB}"
+ IMPORTED_LOCATION "${LIBRARY_LOCATION}"
+ IMPORTED_NO_SONAME "${LIBRARY_NO_SONAME}"
+ IMPORTED_SONAME "${LIBRARY_SONAME}"
+ LINKER_LANGUAGE C
+ PUBLIC_HEADER "${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
+ SOVERSION "${PROJECT_VERSION_MAJOR}"
+ VERSION "${PROJECT_VERSION}"
+ # \note Cargo exports all of the symbols automatically.
+ WINDOWS_EXPORT_ALL_SYMBOLS "TRUE"
+)
+
+target_compile_definitions(${LIBRARY_NAME} INTERFACE $)
+
+target_include_directories(
+ ${LIBRARY_NAME}
+ INTERFACE
+ "$"
+)
+
+set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
+
+set(THREADS_PREFER_PTHREAD_FLAG TRUE)
+
+find_package(Threads REQUIRED)
+
+set(LIBRARY_DEPENDENCIES Threads::Threads ${CMAKE_DL_LIBS})
+
+if(WIN32)
+ list(APPEND LIBRARY_DEPENDENCIES Bcrypt userenv ws2_32)
+else()
+ list(APPEND LIBRARY_DEPENDENCIES m)
+endif()
+
+target_link_libraries(${LIBRARY_NAME} INTERFACE ${LIBRARY_DEPENDENCIES})
+
+install(
+ FILES $
+ TYPE LIB
+ # \note The basename of an import library output by Cargo is the filename
+ # of its corresponding shared library.
+ RENAME "${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}"
+ OPTIONAL
+)
+
+set(LIBRARY_FILE_NAME "${CMAKE_${LIBRARY_TYPE}_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_${LIBRARY_TYPE}_LIBRARY_SUFFIX}")
+
+install(
+ FILES $
+ RENAME "${LIBRARY_FILE_NAME}"
+ DESTINATION ${LIBRARY_DESTINATION}
+)
+
+install(
+ FILES $
+ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
+)
+
+find_package(Doxygen OPTIONAL_COMPONENTS dot)
+
+if(DOXYGEN_FOUND)
+ set(DOXYGEN_ALIASES "installed_headerfile=\\headerfile ${LIBRARY_NAME}.h <${PROJECT_NAME}/${LIBRARY_NAME}.h>")
+
+ set(DOXYGEN_GENERATE_LATEX YES)
+
+ set(DOXYGEN_PDF_HYPERLINKS YES)
+
+ set(DOXYGEN_PROJECT_LOGO "${CMAKE_SOURCE_DIR}/img/brandmark.png")
+
+ set(DOXYGEN_SORT_BRIEF_DOCS YES)
+
+ set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")
+
+ doxygen_add_docs(
+ ${LIBRARY_NAME}_docs
+ "${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
+ "${CMAKE_SOURCE_DIR}/README.md"
+ USE_STAMP_FILE
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ COMMENT "Producing documentation with Doxygen..."
+ )
+
+ # \note A Doxygen input file isn't a file-level dependency so the Doxygen
+ # command must instead depend upon a target that outputs the file or
+ # it will just output an error message when it can't be found.
+ add_dependencies(${LIBRARY_NAME}_docs ${LIBRARY_NAME}_artifacts)
+endif()
diff --git a/automerge-c/src/actor_id.rs b/automerge-c/src/actor_id.rs
new file mode 100644
index 00000000..e5f75856
--- /dev/null
+++ b/automerge-c/src/actor_id.rs
@@ -0,0 +1,166 @@
+use automerge as am;
+use std::cell::RefCell;
+use std::cmp::Ordering;
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+use std::str::FromStr;
+
+use crate::byte_span::AMbyteSpan;
+use crate::result::{to_result, AMresult};
+
+/// \struct AMactorId
+/// \installed_headerfile
+/// \brief An actor's unique identifier.
+#[derive(Eq, PartialEq)]
+pub struct AMactorId {
+ body: *const am::ActorId,
+ c_str: RefCell