Skip to content

Commit d1612c4

Browse files
authored
Merge pull request #202 from rustledger/feat/192-expand-pads-drop-jsonrpc
feat(rustledger): adopt v0.18.0 engine (ledger@3.1.0), expand pads, drop JSON-RPC backend
2 parents ac00f48 + bb9790c commit d1612c4

16 files changed

Lines changed: 185 additions & 568 deletions

.github/workflows/desktop-release.yml

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ jobs:
4040
- os: ubuntu-22.04
4141
target: x86_64-unknown-linux-gnu
4242
sidecar_name: rustfava-server-x86_64-unknown-linux-gnu
43-
- os: macos-latest
43+
# PyInstaller cannot cross-compile: it emits a binary for the
44+
# runner's own arch. macos-latest is now Apple Silicon, so the x64
45+
# sidecar must build on a real Intel runner (issue #193).
46+
- os: macos-13
4447
target: x86_64-apple-darwin
4548
sidecar_name: rustfava-server-x86_64-apple-darwin
4649
- os: macos-14
@@ -66,13 +69,17 @@ jobs:
6669
env:
6770
GH_TOKEN: ${{ github.token }}
6871

69-
- name: Download rustledger WASM from release
72+
- name: Download rustledger component WASM from release
7073
shell: bash
74+
# The in-process WASI Preview 2 component is the engine; the frozen app
75+
# bundles it (see contrib/pyinstaller_spec.spec) so it need not download
76+
# at first run. The legacy rustledger-ffi-wasi-*.wasm is no longer
77+
# shipped upstream (rustledger #1419).
7178
run: |
7279
gh release download ${{ steps.rustledger-tag.outputs.tag }} \
7380
--repo ${{ env.RUSTLEDGER_REPO }} \
74-
--pattern "rustledger-ffi-wasi-*.wasm" \
75-
--output src/rustfava/rustledger/rustledger-wasi.wasm
81+
--pattern "rustledger-ffi-component-*.wasm" \
82+
--output src/rustfava/rustledger/rustledger_ffi_component.wasm
7683
env:
7784
GH_TOKEN: ${{ github.token }}
7885

@@ -115,6 +122,13 @@ jobs:
115122
print(f'Compiled {po} -> {mo}')
116123
"
117124
125+
- name: Set RUSTFAVA_VERSION from tag
126+
# The spec bakes this into rustfava/_version.py so the frozen binary
127+
# reports the real version without dist-info metadata (issue #191).
128+
if: startsWith(github.ref, 'refs/tags/')
129+
shell: bash
130+
run: echo "RUSTFAVA_VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_ENV"
131+
118132
- name: Build sidecar with PyInstaller
119133
shell: bash
120134
run: |
@@ -130,6 +144,24 @@ jobs:
130144
run: |
131145
move dist\rustfava.exe dist\${{ matrix.sidecar_name }}
132146
147+
- name: Verify sidecar architecture (macOS)
148+
if: runner.os == 'macOS'
149+
shell: bash
150+
run: |
151+
BIN="dist/${{ matrix.sidecar_name }}"
152+
file "$BIN"
153+
case "${{ matrix.target }}" in
154+
x86_64-*) EXPECT="x86_64" ;;
155+
aarch64-*) EXPECT="arm64" ;;
156+
*) echo "::error::Unknown macOS target ${{ matrix.target }}"; exit 1 ;;
157+
esac
158+
# Use `file -b` (no filename) so the sidecar_name — which itself
159+
# contains "x86_64"/"aarch64" — can't false-match the grep.
160+
if ! file -b "$BIN" | grep -qw "$EXPECT"; then
161+
echo "::error::Sidecar arch mismatch: expected $EXPECT for ${{ matrix.target }}, got: $(file -b "$BIN")"
162+
exit 1
163+
fi
164+
133165
- name: Upload sidecar artifact
134166
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
135167
with:
@@ -214,7 +246,9 @@ jobs:
214246
- os: ubuntu-22.04
215247
target: x86_64-unknown-linux-gnu
216248
sidecar_name: rustfava-server-x86_64-unknown-linux-gnu
217-
- os: macos-latest
249+
# Intel runner: builds x86_64 natively so the smoke test can launch
250+
# the binary (an arm64 runner has no Rosetta by default), issue #193.
251+
- os: macos-13
218252
target: x86_64-apple-darwin
219253
sidecar_name: rustfava-server-x86_64-apple-darwin
220254
- os: macos-14
@@ -313,6 +347,26 @@ jobs:
313347
ls -la "$APP_PATH" || { echo "App bundle not found!"; exit 1; }
314348
file "$BINARY"
315349
350+
# Verify the bundled sidecar matches the target arch (issue #193).
351+
# Tauri strips the target triple, so the bundled name is plain
352+
# `rustfava-server` (no arch substring to confuse the grep).
353+
SIDECAR="$APP_PATH/Contents/MacOS/rustfava-server"
354+
if [ -f "$SIDECAR" ]; then
355+
file "$SIDECAR"
356+
case "${{ matrix.target }}" in
357+
x86_64-*) EXPECT="x86_64" ;;
358+
aarch64-*) EXPECT="arm64" ;;
359+
*) echo "::error::Unknown macOS target ${{ matrix.target }}"; exit 1 ;;
360+
esac
361+
if ! file -b "$SIDECAR" | grep -qw "$EXPECT"; then
362+
echo "::error::Bundled sidecar arch mismatch: expected $EXPECT for ${{ matrix.target }}, got: $(file -b "$SIDECAR")"
363+
exit 1
364+
fi
365+
else
366+
echo "::error::Bundled sidecar not found at $SIDECAR"
367+
exit 1
368+
fi
369+
316370
# Run binary directly to capture stdout/stderr and panic backtrace
317371
echo "=== Launching with RUST_BACKTRACE=full ==="
318372
RUST_BACKTRACE=full RUST_LOG=debug "$BINARY" > /tmp/app.log 2>&1 &

README.md

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,9 @@ Download the latest release for your platform from the [Releases page](https://g
4040
| **Windows** | `.exe` installer or `.msi` |
4141
| **Linux** | `.AppImage`, `.deb`, `.rpm`, or `.tar.gz` |
4242

43-
> **Backends**: rustfava runs the rustledger engine as an in-process WebAssembly
44-
> component, so the `wasmtime` Python package ships as a dependency and the
45-
> default backend works out of the box — no separate install needed.
46-
>
47-
> To opt back into the legacy JSON-RPC engine
48-
> (`RUSTFAVA_RUSTLEDGER_BACKEND=jsonrpc`), you must install the
49-
> [wasmtime](https://wasmtime.dev/) **CLI** separately (it is *not* a Python
50-
> dependency); the component backend does not need it:
51-
> ```bash
52-
> curl https://wasmtime.dev/install.sh -sSf | bash
53-
> ```
43+
> **Engine**: rustfava runs the rustledger engine as an in-process WebAssembly
44+
> component, so the `wasmtime` Python package ships as a dependency and works
45+
> out of the box — no separate install needed.
5446
5547
### Other Installation Methods
5648

contrib/pyinstaller_spec.spec

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ if translations_dir.exists():
4848
rel_path = f.relative_to(rustfava_dir)
4949
datas.append((str(f), f"rustfava/{rel_path.parent}"))
5050

51-
# WASM file for rustledger
52-
wasm_file = rustfava_dir / "rustledger" / "rustledger-wasi.wasm"
51+
# Rustledger engine: the in-process WASI Preview 2 component. Bundled when
52+
# present so the frozen app does not have to download it at first run (the
53+
# component_engine cache path is read-only inside the PyInstaller bundle).
54+
wasm_file = rustfava_dir / "rustledger" / "rustledger_ffi_component.wasm"
5355
if wasm_file.exists():
5456
datas.append((str(wasm_file), "rustfava/rustledger"))
5557

docs/deployment.md

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,15 @@
22

33
There are several ways to deploy rustfava depending on your needs.
44

5-
## Backends
6-
7-
rustfava runs the rustledger engine in one of two ways, selected by the
8-
`RUSTFAVA_RUSTLEDGER_BACKEND` environment variable:
9-
10-
- **`component`** (default) — runs the rustledger WebAssembly component
11-
in-process via the `wasmtime` Python package, which is installed
12-
automatically as a dependency. Nothing extra to set up.
13-
- **`jsonrpc`** (`json-rpc`/`json`) — the legacy engine, which shells out to the
14-
`wasmtime` **CLI** over JSON-RPC. The CLI is **not** a Python dependency, so
15-
you must install it separately for this backend:
16-
17-
```bash
18-
curl https://wasmtime.dev/install.sh -sSf | bash
19-
```
20-
21-
If `RUSTFAVA_RUSTLEDGER_BACKEND` is unset (or set to anything other than a
22-
JSON-RPC alias) the component backend is used. As a safety net, if the
23-
`wasmtime` package can't be imported, rustfava falls back to the JSON-RPC engine
24-
— which then requires the CLI above.
5+
## Engine
6+
7+
rustfava runs the rustledger engine as a WebAssembly component in-process via
8+
the `wasmtime` Python package, which is installed automatically as a
9+
dependency. Nothing extra to set up.
10+
11+
> The legacy JSON-RPC engine (which shelled out to the `wasmtime` CLI) was
12+
> removed in rustfava 1.31 after its embedding surface was retired upstream;
13+
> `RUSTFAVA_RUSTLEDGER_BACKEND` is no longer used.
2514
2615
## Desktop App
2716

frontend/test/end-to-end-validation.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,7 @@ test("validate journal", async () => {
6767
const res = array(entryValidator)(data);
6868
ok(res.is_ok);
6969
const entries = res.unwrap();
70-
equal(entries.length, 23);
70+
// 24 (was 23): the example ledger's `pad` now materializes a Padding
71+
// transaction in the journal (rustfava #192, engine expand-pads).
72+
equal(entries.length, 24);
7173
});

src/rustfava/rustledger/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from __future__ import annotations
88

99
from rustfava.rustledger.backend import get_engine
10-
from rustfava.rustledger.engine import RustledgerEngine
1110
from rustfava.rustledger.loader import load_string
1211
from rustfava.rustledger.loader import load_uncached
1312

@@ -25,7 +24,6 @@ def is_encrypted_file(path: str) -> bool:
2524

2625

2726
__all__ = [
28-
"RustledgerEngine",
2927
"get_engine",
3028
"is_encrypted_file",
3129
"load_string",

src/rustfava/rustledger/backend.py

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
"""Select the rustledger engine backend.
1+
"""Resolve the rustledger engine backend.
22
33
The in-process component engine (WASI Preview 2 / Component Model, rustledger
4-
#1384) is the **default**; set ``RUSTFAVA_RUSTLEDGER_BACKEND=jsonrpc`` to opt
5-
back into the legacy JSON-RPC engine. The flip is tracked in #173.
4+
#1384) is the only backend. The legacy wasmtime-CLI JSON-RPC engine was removed
5+
once its wasip1 embedding surface was retired upstream (rustledger #1419 /
6+
rustfava #173), so ``RUSTFAVA_RUSTLEDGER_BACKEND`` is no longer consulted.
67
78
This lives in its own module — not the package ``__init__`` — so call sites can
89
import :func:`get_engine` at module level without a circular import through the
@@ -11,44 +12,25 @@
1112

1213
from __future__ import annotations
1314

14-
import os
15-
import shutil
1615
from typing import Any
1716

18-
from rustfava.rustledger.engine import RustledgerEngine
1917
from rustfava.rustledger.engine import RustledgerError
2018

21-
# Values of ``RUSTFAVA_RUSTLEDGER_BACKEND`` that select the legacy engine.
22-
_JSONRPC_ALIASES = frozenset({"jsonrpc", "json-rpc", "json"})
23-
2419

2520
def get_engine() -> Any:
26-
"""Return the engine selected by ``RUSTFAVA_RUSTLEDGER_BACKEND``.
27-
28-
The in-process component engine is the default; ``jsonrpc`` (or
29-
``json-rpc``/``json``) selects the legacy JSON-RPC engine.
30-
"""
31-
backend = os.environ.get("RUSTFAVA_RUSTLEDGER_BACKEND", "").lower()
32-
if backend in _JSONRPC_ALIASES:
33-
return RustledgerEngine.get_instance()
21+
"""Return the in-process rustledger component engine."""
3422
try:
3523
from rustfava.rustledger.component_engine import ( # noqa: PLC0415
3624
get_component_engine,
3725
)
3826
except ImportError as exc:
39-
# The default component backend needs the `wasmtime` Python package.
40-
# The legacy JSON-RPC engine instead needs an external `wasmtime` CLI
41-
# binary that most users don't have — silently falling back to it just
42-
# turns "component unavailable" into a cryptic "wasmtime not found" /
43-
# "Empty response" later (rustfava #136/#120). Only fall back if the
44-
# CLI is actually present; otherwise fail with an actionable message.
45-
if shutil.which("wasmtime"):
46-
return RustledgerEngine.get_instance()
27+
# The component backend needs the `wasmtime` Python package, a required
28+
# rustfava dependency. A missing import means a broken install rather
29+
# than a configuration choice — fail with an actionable message.
4730
msg = (
4831
"The rustledger component backend is unavailable: the `wasmtime` "
4932
"package could not be imported. Reinstall rustfava (wasmtime is a "
50-
"required dependency), or install the wasmtime CLI and set "
51-
"RUSTFAVA_RUSTLEDGER_BACKEND=jsonrpc."
33+
"required dependency)."
5234
)
5335
raise RustledgerError(msg) from exc
5436
return get_component_engine()

src/rustfava/rustledger/component_engine.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,15 @@
5151
from rustfava.rustledger.engine import RUSTLEDGER_VERSION
5252
from rustfava.rustledger.engine import RustledgerError
5353

54-
# The four exported WIT interfaces (package ``rustledger:ledger@2.1.0``).
55-
_LEDGER = "rustledger:ledger/ledger@2.1.0"
56-
_BUILDER = "rustledger:ledger/builder@2.1.0"
57-
_UTIL = "rustledger:ledger/util@2.1.0"
58-
_FORMAT = "rustledger:ledger/format@2.1.0"
54+
# The exported WIT interfaces of package ``rustledger:ledger``. The interface
55+
# IDs embed the full WIT package version (independent of the rustledger release
56+
# version), so a WIT bump means updating ``_WIT_VERSION`` here — a mismatch
57+
# makes ``get_export_index`` return ``None`` and every call fail.
58+
_WIT_VERSION = "3.1.0"
59+
_LEDGER = f"rustledger:ledger/ledger@{_WIT_VERSION}"
60+
_BUILDER = f"rustledger:ledger/builder@{_WIT_VERSION}"
61+
_UTIL = f"rustledger:ledger/util@{_WIT_VERSION}"
62+
_FORMAT = f"rustledger:ledger/format@{_WIT_VERSION}"
5963

6064

6165
_COMPONENT_WASM_URL = (
@@ -616,14 +620,23 @@ def version(self) -> str:
616620
"""Return the component's ``api_version`` string (e.g. ``"2.1"``)."""
617621
return self._call(_LEDGER, "version", [])
618622

619-
def load(self, source: str, filename: str = "<stdin>") -> dict[str, Any]:
623+
def load(
624+
self,
625+
source: str,
626+
filename: str = "<stdin>",
627+
*,
628+
expand_pads: bool = False,
629+
) -> dict[str, Any]:
620630
"""Parse + book ``source``; returns entries/errors/options/....
621631
622-
``filename`` is recorded as the directives' source location (the WIT
623-
``load`` takes it, matching the JSON-RPC engine).
632+
``filename`` is the directives' source location. When ``expand_pads``
633+
is true the engine materializes ``pad`` directives into synthesized
634+
``Padding`` transactions (sorted by date, no source location) so
635+
balance-computing consumers see padded balances; the default keeps the
636+
source-faithful stream (rustledger #1628 / rustfava #192).
624637
"""
625638
self._ensure_version()
626-
return self._call(_LEDGER, "load", [source, filename])
639+
return self._call(_LEDGER, "load", [source, filename, expand_pads])
627640

628641
def query(self, source: str, query_string: str) -> dict[str, Any]:
629642
"""Run a BQL query over ``source``; returns columns/rows/errors."""
@@ -656,9 +669,15 @@ def load_full(
656669
*,
657670
allow_unrestricted_includes: bool = False,
658671
plugins: list[str] | None = None,
672+
expand_pads: bool = False,
659673
) -> dict[str, Any]:
660674
"""Load a file (resolving includes/plugins) via the component.
661675
676+
When ``expand_pads`` is true the engine materializes ``pad`` directives
677+
into synthesized ``Padding`` transactions so balance-computing
678+
consumers see padded balances; the default keeps the source-faithful
679+
stream (rustledger #1628 / rustfava #192).
680+
662681
Unlike the source-based calls this needs filesystem access, so it runs
663682
on a fresh instance with the file's directory pre-opened into the WASI
664683
sandbox at ``/work`` and the path rewritten accordingly.
@@ -681,7 +700,12 @@ def load_full(
681700
inst,
682701
_LEDGER,
683702
"load-file",
684-
[guest_path, allow_unrestricted_includes, plugins or []],
703+
[
704+
guest_path,
705+
allow_unrestricted_includes,
706+
plugins or [],
707+
expand_pads,
708+
],
685709
)
686710
# The component sees files under the WASI pre-open mount (``/work``),
687711
# so the directives it returns carry guest paths in ``meta.filename``

0 commit comments

Comments
 (0)