Rust SDK: in-process mesh node from cargo (design + trial) - #690
Closed
michaelneale wants to merge 19 commits into
Closed
Rust SDK: in-process mesh node from cargo (design + trial)#690michaelneale wants to merge 19 commits into
michaelneale wants to merge 19 commits into
Conversation
…t build time Adds crates/mesh-llm-native-sdk: a small Rust crate whose build.rs downloads the platform/backend-matching libmeshllm_ffi.a from a GitHub release tarball, verifies sha256, extracts it, and emits link directives. Mirrors the shape SwiftPM .binaryTarget gives Swift apps today: prebuilt static archive fetched at build time, static-linked into the consumer's final binary. No dylib to bundle, no native build on the consumer's machine. Backend selection via cargo features (metal, cpu, cuda, rocm, vulkan). Default URL is a GitHub release URL constructed from CARGO_PKG_VERSION; override via MESH_LLM_NATIVE_TARBALL_URL for local trials and offline builds. Verified end-to-end with a trivial consumer outside the workspace linking against a locally-packaged static archive. Design doc: docs/design/RUST_NATIVE_SDK.md.
Rewrite docs/design/RUST_NATIVE_SDK.md to describe what's actually committed: - model is build.rs fetches a static archive from a GitHub release URL, same shape as Swift's .binaryTarget; no native bytes inside the .crate payload - consumer binary is one self-contained executable, no .dylib to bundle - artifact is libmeshllm_ffi.a (static archive), not a dylib - explicit override env vars: MESH_LLM_NATIVE_TARBALL_URL, MESH_LLM_NATIVE_TARBALL_SHA256, MESH_LLM_NATIVE_CACHE_DIR - status section lists what works on this branch (trial verified) and what's not yet wired (release pipeline asset, mesh-llm-api-server feature, publish-chain 429 fix) - includes the trial commands and the otool -L proof of static linking Drops the prior phases that assumed a binary-in-crate model and crates.io size limit negotiation.
Adds crates/mesh-llm-native-sdk/src/ffi.rs with hand-written Rust wrappers over the UniFFI C ABI symbols exported by the static archive: - RustBuffer / RustCallStatus / ForeignBytes mirror types - rustbuffer_free helper for safely consuming UniFFI string returns - generate_owner_keypair_hex() as the first real wrapper UniFFI 0.31 does not ship a Rust bindings generator (only Swift, Kotlin, Python, Ruby), so wrappers are written by hand to keep the single-shared-static-archive model (one artifact for Swift, Kotlin, Node, and Rust). Module is mechanical and small; the design doc calls out the maintenance trade-off vs publishing a Rust-native source crate graph to crates.io. Verified end-to-end: a faux consumer outside the workspace calls mesh_llm_native_sdk::generate_owner_keypair_hex() and gets back fresh 128-character hex keypairs (different bytes each call), proving real mesh-llm code inside the static archive is reachable and runs.
Adds an additive code path in skippy-ffi/build.rs: when SKIPPY_LLAMA_TARBALL_URL is set, download the tarball, verify sha256 against the .sha256 sidecar (or SKIPPY_LLAMA_TARBALL_SHA256), extract into a per-user cache, and set SKIPPY_LLAMA_BUILD_DIR to the extracted root. The rest of the build script proceeds as if the consumer had run 'just llama-build' themselves. This unlocks the Option B Rust SDK story: consumers depend on mesh-llm-api-server + mesh-llm-host-runtime as normal Rust source crates, and skippy-ffi fetches the prebuilt llama.cpp/skippy static archives at consumer build time. No FFI wrappers on the consumer side; the public Rust API (MeshNode::builder(), MeshClient, etc.) is called directly. Workspace-internal builds are unaffected (env var unset -> original behavior using .deps/llama-build). Override env vars: - SKIPPY_LLAMA_TARBALL_URL: file:// or https:// URL - SKIPPY_LLAMA_TARBALL_SHA256: expected hex; otherwise fetched from <url>.sha256 sibling - SKIPPY_LLAMA_CACHE_DIR: cache root (default ~/.cache/skippy-llama-stage) - SKIPPY_LLAMA_TARBALL_FLAVOR: cpu|metal|cuda|... (default inferred from target triple) Verified end-to-end: a Rust app outside the workspace (/tmp/sprout-faux2) depends on mesh-llm-api-server + mesh-llm-host-runtime by path, builds with SKIPPY_LLAMA_TARBALL_URL pointing at a 5 MB locally-packaged tarball, links successfully, runs mesh_llm_api_server::OwnerKeypair::generate() and produces different real ed25519 keypairs on each run.
Drops the mesh-llm-native-sdk crate (Option A, FFI wrappers over UniFFI ABI). The earlier trial proved it works, but Option B — pure Rust source crates on crates.io with skippy-ffi fetching prebuilt llama.cpp static archives from a release tarball — is the better fit: - No FFI wrappers in consumer code; consumers call the public Rust API (MeshNode::builder(), OwnerKeypair, etc.) directly. - 5 MB tarball per platform/backend (just patched llama.cpp .a files) instead of 131 MB (full libmeshllm_ffi.a). - No ongoing hand-written wrapper maintenance as the SDK surface grows. - Existing skippy-ffi link logic (search dirs, link directives) reused unchanged. Rewrites docs/design/RUST_NATIVE_SDK.md to describe Option B as the chosen path, with the local trial reproduction commands, the three remaining publish-side tasks, and explicit caveats.
A working end-to-end consumer that exercises the design proposed in docs/design/RUST_NATIVE_SDK.md: - declares its own [workspace] table so it depends on mesh-llm crates the way an external app would - generates an owner keypair via OwnerKeypair::generate() - runs the equivalent of 'mesh-llm client --auto' through create_auto_node(owner, PublicMeshQuery::default()) - starts the in-process node, lists the models the mesh exposes, cleanly stops Verified locally against the live public mesh: discovers a 5-node mesh serving real models (MiniMax-M2.5, Qwen3-8B, Qwen3.5-9B), starts the node, prints the selected mesh, stops cleanly. Build instructions in examples/rust-sdk-trial/README.md. Requires SKIPPY_LLAMA_TARBALL_URL pointing at a locally-packaged tarball of the patched-llama.cpp static archives (file:// for now; same shape will work with the release-asset URL once the release pipeline ships those tarballs).
Follow-on to docs/design/RUST_NATIVE_SDK.md. Investigates what it would take for the mesh-llm shipped binary to consume mesh-llm-api-server the same way an external Rust app does, instead of reaching into host-runtime internals. Findings: - The user-facing CLI subcommands (discover, download, models, blackboard) already have direct SDK equivalents. The duplication is roughly 400 lines of host-runtime-internal access in crates/mesh-llm-host-runtime/src/cli/commands/*. - Three change groups: (1) domain commands route through SDK, (2) serve/client route through run_serve(MeshServeSpec) from PR #641, (3) auth either moves into the SDK or is explicitly marked binary-only. - Concrete cost: 400-1000 lines re-pointed at SDK calls, plus the auth decision. Not a 7K-line rewrite. CLI shell (clap parsing, output, TUI) stays as the binary's job. - Several CLI surfaces explicitly stay bespoke: update, model-prepare, benchmark, gpu enumerate, stop, http-to-management-api commands. Sequencing depends on landing #690, the gated-relay split, and #691 first. Without mesh-llm-api-server actually on crates.io, the 'binary uses the SDK' story is internal-only.
First mechanical step of the 'CLI on top of SDK' design in
docs/design/CLI_ON_TOP_OF_SDK.md.
cli::commands::discover::run_nostr_discover previously called
crate::network::nostr::discover(...) directly. It now goes through
mesh_llm_api_server::discover_public_meshes(PublicMeshQuery { ... }) -
the same surface external Rust consumers (and examples/rust-sdk-trial/)
use.
A small local lift_public_mesh helper converts each PublicMesh back
into the host-runtime DiscoveredMesh shape so the existing CLI
display, scoring (nostr::score_mesh), and Display impl stay untouched.
No UX change.
Verified against the live public mesh: 'mesh-llm discover',
'mesh-llm discover --name <name>', and 'mesh-llm discover --min-vram N'
all produce identical output to before.
Docs (docs/design/CLI_ON_TOP_OF_SDK.md) updated to reflect what was
achievable in Change 1 and what's blocked on SDK extensions:
- download is blocked on the SDK's MeshModels::download lacking a
progress callback. Switching today would silently lose the
CLI's terminal progress bar.
- The 'models' subcommand uses a lot of host-runtime-internal model
helpers (layered packages, capability introspection, usage records,
catalog/HF search variants) that have no SDK equivalent and
shouldn't grow on speculation.
No behaviour change. Just brings the file into compliance with 'cargo fmt --check' after the new fetch_and_extract_llama_stage code.
Every workspace-internal path-dep that was missing a version specifier
now carries both:
foo = { version = "0.66.0", path = "../foo" }
This is the prerequisite for those crates to be consumable from
crates.io once they're published: cargo resolves path-deps from the
local checkout, but external consumers pulling from the registry need
a version constraint. Without this, the workspace builds fine
internally but an external 'cargo add mesh-llm-api-server' fails to
resolve transitively.
Workspace-internal dep versions match each target crate's own declared
version (almost all are 0.66.0; mesh-mixture-of-agents is 0.1.0).
68 lines patched across 18 crate manifests. The workspace and the
examples/rust-sdk-trial consumer both still build cleanly. A
cargo publish --dry-run on the new chain walks every crate without
errors (some are correctly skipped because their predecessors aren't
on crates.io yet, but the manifests themselves are publishable).
scripts/publish-crates.sh now publishes the full set of crates needed
for an external Rust consumer to depend on mesh-llm-api-server with
the host-runtime feature:
- 18 new crate names added in topological order, interleaved with the
existing 11. The list now covers mesh-llm-host-runtime, the
skippy-* family, mesh-llm-system, openai-frontend, mesh-mixture-of-agents,
mesh-llm-ui, mesh-llm-gpu-bench, model-{package,resolver},
mesh-llm-{guardrails,plugin,identity,protocol,routing,types}.
- unpublished_registry_deps() now reads each crate's own Cargo.toml
rather than carrying a hard-coded dep table, so it stays in sync as
the dep graph evolves. Handles the dir-name vs crate-name mismatch
for mesh-client/ -> mesh-llm-client.
- crate_needs_no_verify() marks the native-linking crates
(skippy-ffi/runtime/server/cache/coordinator/topology/protocol/metrics,
mesh-llm-system, mesh-llm-host-runtime, mesh-llm-node,
model-package, model-resolver) for --no-verify, because the
packaged tarball's build.rs cannot find .deps/llama-build from
inside target/package/. The release pipeline's existing pre-publish
cargo build is the real gate.
Validated by 'bash scripts/publish-crates.sh --dry-run --allow-dirty'
walking the full chain cleanly: 15 crates fully verify and 14 are
correctly skipped with 'depends on X@0.66.0 not yet on crates.io'
messages that resolve once each predecessor lands.
docs/design/RUST_NATIVE_SDK.md updated:
- Item 2 (publish chain expansion) moved from 'not done' to 'done on
this branch'.
- Item 1 (HTTP 429 / issue #691) is now even more important because
the chain grew by 18 new crate names, ~3x the new-crate-name volume
of v0.66.0.
Each Linux/macOS build_* job in the release workflow now packages the patched llama.cpp static archives from .deps/llama-build/build-stage-abi-<backend>/ into a release-asset-shaped tarball plus sha256 sidecar after the existing cmake build. This is the third and last piece of plumbing needed for an external Rust app to consume mesh-llm via cargo: 1. mesh-llm-api-server (and its deps) on crates.io -- requires #691 to land first; the chain is otherwise prepared by the prior commits on this branch. 2. skippy-ffi's build.rs fetches the prebuilt static archives from a URL -- already on this branch. 3. The release pipeline actually publishes those tarballs -- this commit. Naming matches skippy-ffi/build.rs's default URL construction: llama-stage-<target_triple>-<flavor>.tar.gz, with .sha256 sidecar. Tarballs land in dist/ so they're picked up by the existing upload-artifact -> publish flow that creates GitHub release assets. New script scripts/package-llama-stage.sh extracts the shared packaging logic; verified locally against the same trial that examples/rust-sdk-trial/ exercises (5.1 MB metal tarball, consumer links cleanly, joins the live public mesh). Wired into the Linux/macOS build_* jobs (build, build_linux_arm64, build_linux_cuda, build_linux_cuda_blackwell, build_linux_rocm, build_linux_vulkan). Windows jobs are not wired because they use PowerShell and the package script is bash; deferred to a follow-up. Windows Rust consumers can still consume the SDK by overriding SKIPPY_LLAMA_TARBALL_URL. docs/design/RUST_NATIVE_SDK.md updated to mark item 3 done for Linux/macOS and call out the Windows gap honestly.
…-publish * origin/main: fix(lint): many linter corrections (#696) Upgrade workspace to Rust 2024 edition Fix dispatched Swift release manifest flow Fix metadata-only package verification cache Fix Blacksmith CI discrepancies
…ime feature
Lets a Rust app drive a real mesh-llm node from the published SDK \u2014 the
same iroh-backed peer the binary runs, not the HTTP-shim client that
MeshNode::start() used previously.
```rust
let node = MeshNode::builder()
.identity(OwnerKeypair::generate())
.join(invite)
.role(MeshRole::Client)
.relay("https://gated.example/")
.relay_auth("https://gated.example/", "<bearer-token>")
.max_vram_gb(0.0)
.build()?;
node.start().await?;
let invite = node.invite_token().await;
```
Architecture:
- New `mesh_llm_host_runtime::host_node` module exposes `HostNodeSpec`
+ `HostNode` + `start_host_node` as the curated entry point into the
internal `mesh::Node`. SDK consumers don't see internal types directly.
- `mesh-llm-api-server` gains a `host-runtime` Cargo feature
(off by default). With it on, depends on `mesh-llm-host-runtime` and
`MeshNode::start()` calls `host_node::start_host_node` with the
builder's relay / relay_auth / role / quic_bind / max_vram fields.
- Builder API extended with .role(), .relay(), .relay_auth(),
.quic_bind(), .max_vram_gb(), .no_enumerate_host(). New types
`MeshRole` and `MeshQuicBind` mirror the CLI surface; SDK consumers
never have to import host-runtime-internal types.
- Without the feature, the new builder methods are still callable
(forward-compat: an SDK consumer can configure relay-auth without
caring whether the runtime is wired) \u2014 fields are stored but
ignored, and start() falls back to the existing HTTP-shim behaviour.
- New invite_token() / set_display_name() accessors on MeshNode
(host-runtime-only) so consumers can introspect / advertise their
running node.
Cycle fix: `mesh-llm-host-runtime` had an unused declared dep on
`mesh-llm-api-server` (no `use` sites in src/). Dropped to let the
inverse dep land cleanly.
Test: crates/mesh-llm-api-server/tests/host_node_gated_relay.rs
(gated on host-runtime feature) brings up an in-process iroh-relay with
AccessConfig::Restricted, builds a MeshNode with .relay_auth(...) for
the matching token, and asserts node.start() reaches the gated relay
end-to-end. A second test pins that the wrong token is denied with
'not authorized' at the iroh wire layer.
SDK consumers can now launch the full mesh node *including* the OpenAI
HTTP API surface from Rust code, without going through the binary CLI:
```rust
let node = MeshNode::builder()
.identity(OwnerKeypair::generate())
.join(invite)
.role(MeshRole::Client)
.relay("https://gated.example/")
.relay_auth("https://gated.example/", "<bearer>")
.openai_port(0) // 0 = OS-assigned ephemeral port
.build()?;
node.start().await?;
let base = node.openai_base_url().await.unwrap();
// e.g. http://127.0.0.1:54321
// Hit /v1/chat/completions, /v1/models, /v1/responses there as usual.
```
Mechanism:
- New `mesh_llm_host_runtime::host_node::start_openai_proxy(node, port,
listen_all)` wraps the internal `network::openai::ingress::api_proxy`
with default empty target_rx (routing pulls remote peers dynamically
from `node.hosts_for_model()` at request time) and a background drain
for runtime-control messages (SDK consumers without local serving
have no one to handle Load/Unload control requests).
- `MeshNodeBuilder` gains `.openai_port(port)` and
`.openai_listen_all(bool)` setters. With the `host-runtime` feature on
and `openai_port` set, `MeshNode::start()` binds and spawns the proxy
alongside the mesh node; `MeshNode::stop()` aborts it.
- New `MeshNode::openai_base_url()` accessor returns the bound URL
after start (Some when the proxy is running, None otherwise).
End-to-end test `openai_proxy_binds_and_serves_v1_models_over_http`:
constructs a MeshNode via the SDK with .openai_port(0), GETs /v1/models
over real TCP, asserts 200 OK with a JSON body containing the `data`
field (OpenAI shape), then stops the node and asserts the port no
longer answers.
Scope clarification in host_node module docs: the "this does not do"
list shrinks; OpenAI proxy is no longer in it. Local model serving
still requires plugging an EmbeddedServingController, which is a
separate concern (client-only embedders don't need it because the
proxy routes to remote mesh peers).
Adds the missing piece: SDK consumers can now run *exactly* what the
mesh-llm binary runs \u2014 not a degraded subset. `run_serve(spec)`
constructs argv from a typed `MeshServeSpec` and feeds it to the same
`runtime::run_with_args` entry point the binary calls.
```rust
use mesh_llm_api_server::{run_serve, MeshServeSpec};
run_serve(MeshServeSpec {
client: true,
auto: true,
relays: vec!["https://gated.example/".into()],
relay_auths: [(
"https://gated.example/".to_string(),
"<bearer>".to_string(),
)].into_iter().collect(),
port: Some(9337),
console_port: Some(3131),
max_vram_gb: Some(0.0),
..Default::default()
}).await?;
```
That gets the full thing: auto-discovery, election, tunnel manager,
OpenAI proxy, management console, local model serving (when configured),
plugin host. Same code path as `mesh-llm serve` / `mesh-llm client`.
Mechanism:
- `runtime::run()` split into a thin env-driven entry and a new
`run_with_args(argv)` that takes a caller-supplied argv. Binary
unchanged \u2014 main() still calls run() which forwards std::env::args_os.
- `mesh_llm_host_runtime::run_with_args(argv)` re-exports it at the
crate root.
- `host_node::MeshServeSpec` covers the realistic CLI surface: client,
auto, publish, mesh_name, region, display_name, join, discover,
models, ggufs, mmproj, port, console_port, headless, blackboard,
relays, relay_auths, nostr_relays, bind_port, bind_ip, listen_all,
max_vram_gb, no_enumerate_host, config, owner_key, owner_required,
node_label, trust_owners, debug. Plus an extra_args escape hatch.
- `host_node::run_serve(spec)` serialises the spec to argv via
`MeshServeSpec::into_argv()` and calls `run_with_args`.
- `mesh-llm-api-server::{run_serve, MeshServeSpec}` re-exports them
through the published SDK crate (gated on the host-runtime feature).
Regression-catcher test `mesh_serve_spec_argv_parses_via_the_real_cli_parser`:
constructs a fully-populated MeshServeSpec, calls into_argv(), runs it
through `normalize_runtime_surface_args` + `Cli::try_parse_from` (the
real parser the binary uses), asserts every field round-trips. If a
future refactor renames a CLI flag, this fails immediately and points
at the drifted MeshServeSpec field.
This is what sprout (or any Rust app) actually needs to run a full
mesh-llm node from inside its own process. The earlier
`MeshNodeBuilder` + `start_openai_proxy` work remains useful for
finer-grained client-only embedders that don't want the whole runtime
machinery, but `run_serve` is the answer to 'I want my Rust app to do
exactly what `mesh-llm serve` does.'
Adds the missing 'how do I run mesh-llm from Rust?' answer in three places so it's discoverable however a consumer arrives: - `docs/SDK.md` gains a 'Run the full mesh-llm runtime from Rust (host-runtime feature)' section under Rust Usage. Explains the feature flag, contrasts with MeshNodeBuilder (fine-grained vs full-runtime), gives a complete relay-auth + OpenAI + console example, lists every MeshServeSpec field. - `crates/mesh-llm-api-server/README.md` gets a parallel section so consumers landing on the crate page (e.g. via docs.rs or crates.io) see the run_serve story without leaving the crate docs. - The `pub use` re-export of `run_serve` / `MeshServeSpec` in `mesh-llm-api-server/src/lib.rs` now carries a full rustdoc example with the same MeshServeSpec, so `cargo doc` surfaces it prominently. No code changes; documentation only.
This was referenced May 26, 2026
cargo fmt under edition 2024 sorts uppercase types alongside lowercase modules. Reorders imports in the SDK files cherry-picked from #641. No logic change.
Three small cleanups to make this branch green on a workspace that doesn't (yet) have the rest of #641's gated-relay polish: 1. skippy-ffi/build.rs: collapse the nested ifs in the tarball-URL fetch path into a single let-chain so clippy's collapsible-if doesn't fire. 2. crates/mesh-llm-host-runtime/src/host_node.rs: remove three tests that depend on helpers only present on the gated-relay PR (id_returns_bare_hex_endpoint_id needs the bare-hex HostNode::id() refactor; shutdown_closes_the_mesh_endpoint needs Node::endpoint_is_closed_for_tests; shutdown_releases_fixed_quic_bind depends on the shutdown polish that releases the QUIC bind cleanly). They come back once that work is on main. Also remove the helpers (free_local_udp_port, probe_quic_port_released) those tests pulled in. 3. The mesh_serve_spec_argv_parses_via_the_real_cli_parser test no longer asserts on cli.relay_auth (that field doesn't exist on this branch). Updated to use 'https://public.example/' instead of 'https://gated.example/' since gated-relay support isn't here yet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
Designs and proves the path for a Rust app to depend on mesh-llm as a normal cargo dep and run a real in-process mesh node — the same outcome Swift apps get from
.binaryTargetand Kotlin apps get from the AAR.The shape: a Rust consumer depends on
mesh-llm-api-server+mesh-llm-host-runtimeas normal Rust source crates and calls the public Rust API directly (MeshNode::builder(),OwnerKeypair::generate(),create_auto_node(...)).skippy-ffi'sbuild.rsfetches the prebuilt patched-llama.cpp static archives from a GitHub release tarball at consumer build time. No FFI wrappers in consumer code, no CMake on the consumer's machine, no separatemesh-llmdaemon, no dylib to bundle.Full design and the alternatives considered in
docs/design/RUST_NATIVE_SDK.md.Status
Draft. Design proposal with a working trial on this branch. Not ready to merge — there are three publish-side tasks still outstanding (see "Not done yet" in the design doc). Open to get sign-off on the design before doing the release-pipeline work.
What's verified working on this branch
crates/skippy-ffi/build.rshas a new additive code path: whenSKIPPY_LLAMA_TARBALL_URLis set, it fetches the tarball, verifies sha256, extracts into a per-user cache, and points its existing link logic at the extracted archives. With the env var unset, behavior is identical to today.examples/rust-sdk-trial/lives outside the workspace (declares its own[workspace]table) and depends on mesh-llm crates the way an external app would. Builds with afile://tarball URL pointing at a locally-packaged 5 MB tarball of the patched-llama.cpp static archives.Run against the live public mesh, it does the equivalent of
mesh-llm client --auto:Real public-mesh discovery via Nostr, real node start/stop, real Rust API surface — no FFI in consumer code.
Final binary is 1.7 MB, dynamically linked only to macOS system frameworks. Patched llama.cpp + skippy + mesh-llm host runtime all statically inside.
What's not done
Three things to make this consumable from crates.io (vs path):
mesh-llm-host-runtime,skippy-ffi,skippy-runtime,skippy-server, and other transitively-required crates to crates.io. All pure Rust source;skippy-ffiis self-sufficient via the URL-fetch path on this branch.llama-stage-<triple>-<flavor>.tar.gzper matrix cell. Eachbuild_*job already runsscripts/build-llama.shand produces the archives; add a tar + sha256 + upload-artifact step. Naming must match whatskippy-ffi/build.rsconstructs by default.Reproduce locally
See
examples/rust-sdk-trial/README.mdfor full instructions.Out of scope