From 56cb4a6ba2af29982d626728fa05481dee4ed4c6 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Sun, 6 Sep 2026 16:06:38 +0530
Subject: [PATCH 01/17] sdk%refac(lint): consolidate the formatter check and
rewrite runners
---
maint/common.py | 63 +++++++++++++++++++++++++++++++++++++++
maint/lint/lint_cargo.py | 62 +++++++++++++++++---------------------
maint/lint/lint_codeql.py | 39 +++++++-----------------
3 files changed, 101 insertions(+), 63 deletions(-)
diff --git a/maint/common.py b/maint/common.py
index c45e2c2c..4b0e6d68 100644
--- a/maint/common.py
+++ b/maint/common.py
@@ -157,6 +157,69 @@ def git_out(cwd: Path | str, *args: str) -> str:
return result.stdout.strip()
+# Every formatter had grown this body independently, so the hint, the
+# phrasing and the empty-list case were copies free to drift apart.
+def formatted(
+ script: str,
+ noun: str,
+ sources: list[Path] | None,
+ command: Callable[[list[Path]], list[str]],
+ *,
+ fix: bool,
+ scoped: bool,
+ cwd: Path | None = None,
+ output: Callable[[str, str], None] | None = None,
+) -> int:
+ """Hold *sources* to a formatter, or rewrite them, and say which happened.
+
+ *command* is handed the paths and returns the argv to run. *output* is
+ handed what the tool wrote, as stdout and stderr, by a caller that has to
+ post-process it; left out, the tool writes to this process's own streams.
+
+ *sources* is None where the tool finds its own files, which is the one
+ case a count cannot be reported for.
+ """
+ if sources is not None and not sources:
+ print(f"{script}: no {noun} was touched")
+ return RETCODE_PASS
+
+ result = subprocess.run( # noqa: S603
+ command(sources or []),
+ capture_output=output is not None,
+ check=False,
+ cwd=None if cwd is None else str(cwd),
+ text=True,
+ )
+ if output is not None:
+ output(result.stdout, result.stderr)
+
+ if result.returncode != 0:
+ if not fix:
+ print(
+ f"hint: run 'python3 maint/lint/{script}.py apply-all' to rewrite",
+ file=sys.stderr,
+ )
+ return RETCODE_ERR
+
+ if scoped:
+ scope = f"{len(sources or [])} touched {noun}(s)"
+ elif sources is None:
+ scope = f"every {noun}"
+ else:
+ scope = f"every {noun} ({len(sources)})"
+ print(f"{script}: rewrote {scope}" if fix else f"{script}: {scope} conforms")
+ return RETCODE_PASS
+
+
+def format_verbs(noun: str) -> dict[str, str]:
+ """Return the check/apply/apply-all verbs every formatter declares."""
+ return {
+ "check": f"report every {noun} whose formatting differs",
+ "apply": f"rewrite the {noun} this branch changed vs {DEFAULT_BASE}",
+ "apply-all": f"rewrite every {noun} in the tree",
+ }
+
+
def relay(
text: str,
repo_root: Path,
diff --git a/maint/lint/lint_cargo.py b/maint/lint/lint_cargo.py
index 0eb0082b..a27cf0ff 100755
--- a/maint/lint/lint_cargo.py
+++ b/maint/lint/lint_cargo.py
@@ -30,6 +30,7 @@
RETCODE_SKIP,
declare_verbs,
format_table,
+ formatted,
relay,
require_bin,
root_dir,
@@ -69,42 +70,33 @@ def _check_format(
print(f"{e}, skipping the format check", file=sys.stderr)
return None
- if only is not None and not only:
- print(f"{SCRIPT}: no TOML file was touched")
- return RETCODE_PASS
-
- argv = [taplo, "fmt"] + ([] if fix else ["--check", "--diff"]) + (only or [])
- result = subprocess.run( # noqa: S603
- argv,
- capture_output=True,
- check=False,
- cwd=str(repo_root),
- text=True,
- )
- relay(result.stdout, repo_root)
-
- # Taplo reports the file count on stderr at INFO, so only the lines that
- # name a fault should be emitted.
- relay(
- result.stderr,
- repo_root,
- stream=sys.stderr,
- drop=lambda line: line.lstrip().startswith("INFO"),
- )
-
- if result.returncode != 0:
- if not fix:
- print(
- f"hint: run 'python3 maint/lint/{SCRIPT}.py apply-all' to rewrite",
- file=sys.stderr,
- )
- return RETCODE_ERR
- scope = (
- f"{len(only)} touched TOML file(s)" if only is not None
- else "every TOML file"
+ def shorten(out: str, err: str) -> None:
+ relay(out, repo_root)
+ # Taplo reports the file count on stderr at INFO, so only the lines
+ # that name a fault should be emitted.
+ relay(
+ err,
+ repo_root,
+ stream=sys.stderr,
+ drop=lambda line: line.lstrip().startswith("INFO"),
+ )
+
+ # None, not an empty list: with no paths taplo finds its own through
+ # '.taplo.toml', so there is no count to report for the whole tree.
+ return formatted(
+ SCRIPT,
+ "TOML file",
+ None if only is None else [Path(name) for name in only],
+ lambda paths: [
+ taplo, "fmt",
+ *([] if fix else ["--check", "--diff"]),
+ *[str(p) for p in paths],
+ ],
+ fix=fix,
+ scoped=only is not None,
+ cwd=repo_root,
+ output=shorten,
)
- print(f"{SCRIPT}: rewrote {scope}" if fix else f"{SCRIPT}: {scope} conforms")
- return RETCODE_PASS
def _parse_version(text: str) -> Version:
diff --git a/maint/lint/lint_codeql.py b/maint/lint/lint_codeql.py
index 6d1d3b2b..77cd6f50 100755
--- a/maint/lint/lint_codeql.py
+++ b/maint/lint/lint_codeql.py
@@ -27,12 +27,13 @@
from collections.abc import Iterator
from common import (
- DEFAULT_BASE,
RETCODE_ERR,
RETCODE_PASS,
RETCODE_SKIP,
SOURCE_DIRS,
declare_verbs,
+ format_verbs,
+ formatted,
require_bin,
root_dir,
touched,
@@ -99,34 +100,19 @@ def _format_ql(
only: list[str] | None = None,
) -> int:
"""Check or rewrite the formatting of the QL under `maint/codeql`."""
- sources = _ql_sources(repo_root, only)
- if not sources:
- print(f"{SCRIPT}: no QL file was touched")
- return RETCODE_PASS
-
- result = subprocess.run( # noqa: S603
- [
+ return formatted(
+ SCRIPT,
+ "QL file",
+ _ql_sources(repo_root, only),
+ lambda paths: [
codeql_bin, "query", "format",
*(["-i"] if fix else ["--check-only"]),
"--",
- *[str(p) for p in sources],
+ *[str(p) for p in paths],
],
- check=False,
- )
- if result.returncode != 0:
- if not fix:
- print(
- f"hint: run 'python3 maint/lint/{SCRIPT}.py apply-all' to rewrite",
- file=sys.stderr,
- )
- return RETCODE_ERR
-
- scope = (
- f"{len(sources)} touched QL file(s)" if only is not None
- else f"every QL file ({len(sources)})"
+ fix=fix,
+ scoped=only is not None,
)
- print(f"{SCRIPT}: rewrote {scope}" if fix else f"{SCRIPT}: {scope} conforms")
- return RETCODE_PASS
def _generate_source_lines(
@@ -262,10 +248,7 @@ def _workspace_dirs(
def _parse_args(argv: list[str]) -> argparse.Namespace:
parser = declare_verbs(
"Format the QL, and analyse the workspace with it.",
- {
- "check": "report every QL file whose formatting differs",
- "apply": f"rewrite the QL this branch changed vs {DEFAULT_BASE}",
- "apply-all": "rewrite every QL file in the tree",
+ format_verbs("QL file") | {
"run": "analyse the one language --lang names",
"run-all": "analyse every language whose tools are present",
},
From 0f05528e19123ab966ce8c0ac878c6d5ff5128a2 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 12:39:47 +0530
Subject: [PATCH 02/17] sdk%lint(codeql): add option to suppress known failures
---
maint/lint/lint_codeql.py | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/maint/lint/lint_codeql.py b/maint/lint/lint_codeql.py
index 77cd6f50..7a76e282 100755
--- a/maint/lint/lint_codeql.py
+++ b/maint/lint/lint_codeql.py
@@ -62,6 +62,8 @@ class Language(NamedTuple):
pack: str
# Binaries it needs; absent, the language skips rather than fails.
requires: tuple[str, ...]
+ # Findings to drop, each matched whole as (path, message).
+ suppressions: tuple[tuple[str, str], ...] = ()
# Every language this harness knows, in the order `run-all` walks them.
@@ -169,8 +171,11 @@ def _generate_source_lines(
return out
-def _print_csv_diagnostics(results_path: Path) -> int:
- """Print CSV results to stderr. Returns the finding count."""
+def _print_csv_diagnostics(
+ results_path: Path,
+ suppressions: tuple[tuple[str, str], ...],
+) -> int:
+ """Print CSV results to stderr. Returns the unsuppressed finding count."""
count = 0
with results_path.open(newline="") as f:
for row in csv.reader(f):
@@ -181,6 +186,8 @@ def _print_csv_diagnostics(results_path: Path) -> int:
uri = Path(row[4].lstrip("/"))
line = row[5]
msg = row[3].replace("\n", " ")
+ if (str(uri), msg) in suppressions:
+ continue
print(f"{uri}:{line}: {msg}", file=sys.stderr)
count += 1
return count
@@ -398,7 +405,10 @@ def _analyse(
check=True,
)
- total_findings = _print_csv_diagnostics(results_path)
+ total_findings = _print_csv_diagnostics(
+ results_path,
+ language.suppressions,
+ )
return RETCODE_ERR if total_findings > 0 else RETCODE_PASS
From 404dee1c566479dbc73f3a5cb1ac6a8bda673cff Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 12:41:55 +0530
Subject: [PATCH 03/17] sdk%lint(codeql): suppress `Variable 'None' is not
used` for now
---
maint/lint/lint_codeql.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/maint/lint/lint_codeql.py b/maint/lint/lint_codeql.py
index 7a76e282..79dce369 100755
--- a/maint/lint/lint_codeql.py
+++ b/maint/lint/lint_codeql.py
@@ -66,6 +66,10 @@ class Language(NamedTuple):
suppressions: tuple[tuple[str, str], ...] = ()
+_SUPPRESSIONS_RS = (
+ ("pkgs/primitives/src/types/netinfo.rs", "Variable 'None' is not used."),
+)
+
# Every language this harness knows, in the order `run-all` walks them.
LANGUAGES: tuple[Language, ...] = (
Language(
@@ -73,6 +77,7 @@ class Language(NamedTuple):
directory="rust",
pack="codeql/rust-queries",
requires=("rustc",),
+ suppressions=_SUPPRESSIONS_RS,
),
)
From 1f873366b39f0da18da46aeea06914e2854c691d Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 03:56:23 +0530
Subject: [PATCH 04/17] sdk%fix(docs): continue lists at the CommonMark indent
---
docs/preprocess.py | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/docs/preprocess.py b/docs/preprocess.py
index e2fccf70..4e50daf8 100644
--- a/docs/preprocess.py
+++ b/docs/preprocess.py
@@ -18,6 +18,11 @@
from typing import TYPE_CHECKING
from common import off_disk, root_dir, spelt_as_stored
+from markdown.blockprocessors import (
+ ListIndentProcessor,
+ OListProcessor,
+ UListProcessor,
+)
from markdown.extensions import Extension
from markdown.preprocessors import Preprocessor
@@ -330,6 +335,22 @@ def _section(lines: list[str], name: str, spec: str) -> list[str]:
return found
+# Indentation where CommonMark defines a continued list item.
+_LIST_INDENT = 2
+
+
+def _commonmark_list_indent(md: Markdown) -> None:
+ """Rebuild the list processors at CommonMark indentation."""
+ kept = md.tab_length
+ md.tab_length = _LIST_INDENT
+ for processor in md.parser.blockprocessors:
+ if isinstance(
+ processor, ListIndentProcessor | OListProcessor | UListProcessor
+ ):
+ processor.__init__(md.parser)
+ md.tab_length = kept
+
+
class PreprocessorHost(Extension):
"""Markdown extension entrypoint."""
@@ -348,6 +369,7 @@ def extendMarkdown(self, md: Markdown) -> None:
)
md.preprocessors.register(include, "include", 32)
md.preprocessors.register(GfmAlertsPreprocessor(md), "gfm_alerts", 31)
+ _commonmark_list_indent(md)
def makeExtension(**kwargs: object) -> PreprocessorHost:
@@ -382,6 +404,11 @@ def _scratch(**files: str) -> Iterator[Path]:
(home / f"{stem}.md").write_text(text, encoding="utf-8")
yield home.relative_to(root_dir())
+ def test_list_continues_at_the_commonmark_column(self) -> None:
+ out = self._render("* lead\n\n continuation\n")
+ assert out.count("
") == 1
+ assert "continuation" in out.split("")[0]
+
def test_alert_becomes_admonition(self) -> None:
out = self._render("> [!CAUTION]\n> Mind the gap.\n")
assert 'class="admonition danger"' in out
From b8507e6198dd5eaf2f04be45fc1f49f46a5df9f7 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Mon, 7 Sep 2026 18:49:50 +0530
Subject: [PATCH 05/17] sdk%feat(nix): create minimum Python and Rust devshell
---
.vscode/extensions.json | 1 +
.vscode/settings.json | 4 ++
contrib/nix/README.md | 40 ++++++++++++
contrib/nix/flake.lock | 120 +++++++++++++++++++++++++++++++++++
contrib/nix/flake.nix | 77 ++++++++++++++++++++++
contrib/nix/mods/nixpkgs.nix | 17 +++++
contrib/nix/mods/python.nix | 70 ++++++++++++++++++++
contrib/nix/mods/rust.nix | 11 ++++
contrib/nix/shell/ci.nix | 9 +++
contrib/nix/shell/common.nix | 48 ++++++++++++++
docs/dev/devshells.md | 32 ++++++++++
docs/dev/getting_started.md | 5 ++
docs/zensical.toml | 1 +
maint/README.md | 1 +
maint/lint/lint_nix.py | 86 +++++++++++++++++++++++++
rust-toolchain.toml | 4 +-
16 files changed, 524 insertions(+), 2 deletions(-)
create mode 100644 contrib/nix/README.md
create mode 100644 contrib/nix/flake.lock
create mode 100644 contrib/nix/flake.nix
create mode 100644 contrib/nix/mods/nixpkgs.nix
create mode 100644 contrib/nix/mods/python.nix
create mode 100644 contrib/nix/mods/rust.nix
create mode 100644 contrib/nix/shell/ci.nix
create mode 100644 contrib/nix/shell/common.nix
create mode 100644 docs/dev/devshells.md
create mode 100755 maint/lint/lint_nix.py
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 5edfba6d..91a5e507 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -7,5 +7,6 @@
"ms-vscode-remote.remote-containers",
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
+ "jnoortheen.nix-ide",
]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index c3cec8ca..5f7eace3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,8 @@
{
+ "[nix]": {
+ "editor.defaultFormatter": "jnoortheen.nix-ide",
+ "editor.rulers": [80, 100]
+ },
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.rulers": [80, 120],
diff --git a/contrib/nix/README.md b/contrib/nix/README.md
new file mode 100644
index 00000000..dd6d5a85
--- /dev/null
+++ b/contrib/nix/README.md
@@ -0,0 +1,40 @@
+## Nix
+
+To maintain a consistent development environment and reproducible toolchain, a declarative environment is available
+using [Nix](https://nixos.org) on macOS and Linux hosts on ARM64 and AMD64. **Windows users are recommended to either
+resort to using Nix through [Windows Subsystem for Linux](https://github.com/microsoft/WSL) or manually set up their
+environment.**
+
+### Setting up Nix
+
+> [!WARNING]
+>
+> macOS 26 "Tahoe" is the last release supporting Intel-based Macs
+> ([source](https://developer.apple.com/videos/play/wwdc2025/102/?time=3296)). Support for it as a _host_ is on a
+> best-effort basis while it is still an officially supported _target_ platform. `nixpkgs` dropped `x86_64-darwin`
+> per [NixOS/nixpkgs#535508](https://github.com/NixOS/nixpkgs/pull/535508) (included in 26.11).
+>
+> The environment is therefore pinned to its prior release, 26.05, and will stay there for as long as it remains
+> reasonable.
+
+For install guidance on Linux, see [here](https://nixos.org/download/#nix-install-linux). For macOS, while Nix is an
+option, Determinate Nix has been found to better accommodate macOS-specific quirks and guidance for that is available
+[here](https://docs.determinate.systems/determinate-nix/#getting-started). That being said, regardless of choice of Nix
+distribution used (including independent projects like [Lix](https://lix.systems/install/)), `nix-command` and `flakes`
+features need to be enabled (guidance for enablement should be taken from your distribution vendor).
+
+### Entering a shell
+
+To enter an interactive shell, from the repository root, use
+
+```bash
+nix develop ./contrib/nix#ci
+```
+
+### One-shot commands
+
+To execute a command _without_ switching to a shell; or for scripting, use
+
+```bash
+nix develop ./contrib/nix#ci --command cargo test --workspace --features full
+```
diff --git a/contrib/nix/flake.lock b/contrib/nix/flake.lock
new file mode 100644
index 00000000..3c880c22
--- /dev/null
+++ b/contrib/nix/flake.lock
@@ -0,0 +1,120 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1787962033,
+ "narHash": "sha256-u6z9VTZA4Kf3RkHQo9sQI7NI4Ei/uiU9vrMqOiwWP1Y=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "c5c4a43b0e8056328ec4529f735cabdb8f1942bb",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-26.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "pyproject-build-systems": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "pyproject-nix": [
+ "pyproject-nix"
+ ],
+ "uv2nix": [
+ "uv2nix"
+ ]
+ },
+ "locked": {
+ "lastModified": 1788149501,
+ "narHash": "sha256-XKJP4KvhawV3pyVfraX3sflz05Rp7KP74gRt6QqKjNE=",
+ "owner": "pyproject-nix",
+ "repo": "build-system-pkgs",
+ "rev": "150839ac67b5a34db56a55e8f6b7099a4e7878ab",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "build-system-pkgs",
+ "type": "github"
+ }
+ },
+ "pyproject-nix": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1786031528,
+ "narHash": "sha256-cROiHKO3UbIKqF5FG5NikvydzlfIj4EcR1Cty9qOVt4=",
+ "owner": "pyproject-nix",
+ "repo": "pyproject.nix",
+ "rev": "1b1485546d85f6f6c7aadb10c4923dbc09633263",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "pyproject.nix",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs",
+ "pyproject-build-systems": "pyproject-build-systems",
+ "pyproject-nix": "pyproject-nix",
+ "rust-overlay": "rust-overlay",
+ "uv2nix": "uv2nix"
+ }
+ },
+ "rust-overlay": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1788077403,
+ "narHash": "sha256-c20YZQKzQ27qC6Wh729fWRTpI7oV4+7Ur4icKssD4Wc=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "89e26eeaafa88a2ede4778734acc794ff0299beb",
+ "type": "github"
+ },
+ "original": {
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "type": "github"
+ }
+ },
+ "uv2nix": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "pyproject-nix": [
+ "pyproject-nix"
+ ]
+ },
+ "locked": {
+ "lastModified": 1788001239,
+ "narHash": "sha256-AELmsXPI546MhbC/ZXC7WRUkCz7d4rqKTHUmliIgPpI=",
+ "owner": "pyproject-nix",
+ "repo": "uv2nix",
+ "rev": "7f9c6b613d2e749e54854b1d60ab6a2192db889e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "uv2nix",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/contrib/nix/flake.nix b/contrib/nix/flake.nix
new file mode 100644
index 00000000..a494422e
--- /dev/null
+++ b/contrib/nix/flake.nix
@@ -0,0 +1,77 @@
+# Development shells for the Dash Base SDK
+
+{
+ description = "Development shells for the Dash Base SDK";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
+
+ pyproject-build-systems = {
+ url = "github:pyproject-nix/build-system-pkgs";
+ inputs.nixpkgs.follows = "nixpkgs";
+ inputs.pyproject-nix.follows = "pyproject-nix";
+ inputs.uv2nix.follows = "uv2nix";
+ };
+
+ pyproject-nix = {
+ url = "github:pyproject-nix/pyproject.nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ rust-overlay = {
+ url = "github:oxalica/rust-overlay";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ uv2nix = {
+ url = "github:pyproject-nix/uv2nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ inputs.pyproject-nix.follows = "pyproject-nix";
+ };
+ };
+
+ outputs =
+ {
+ nixpkgs,
+ rust-overlay,
+ ...
+ }@inputs:
+ let
+ inherit (nixpkgs) lib;
+
+ systems = [
+ "x86_64-linux"
+ "aarch64-linux"
+ "x86_64-darwin"
+ "aarch64-darwin"
+ ];
+
+ eachSystem =
+ f:
+ lib.genAttrs systems (
+ system:
+ f (
+ import nixpkgs {
+ inherit system;
+ overlays = [ rust-overlay.overlays.default ];
+ }
+ )
+ );
+ in
+ {
+ devShells = eachSystem (
+ pkgs:
+ let
+ ctx = import ./shell/common.nix {
+ inherit pkgs lib inputs;
+ root = ../..;
+ };
+ in
+ {
+ ci = import ./shell/ci.nix ctx;
+ }
+ );
+
+ formatter = eachSystem (pkgs: pkgs.nixfmt);
+ };
+}
diff --git a/contrib/nix/mods/nixpkgs.nix b/contrib/nix/mods/nixpkgs.nix
new file mode 100644
index 00000000..c02b5b61
--- /dev/null
+++ b/contrib/nix/mods/nixpkgs.nix
@@ -0,0 +1,17 @@
+# Native packages sourced from the Nix Packages collection (nixpkgs)
+
+{ pkgs }:
+
+{
+ packages = [
+ pkgs.git
+ pkgs.nixfmt
+ pkgs.nodejs_24
+
+ # Packages synced with '.tools' from 'pyproject.toml'.
+ pkgs.ruff
+ pkgs.semgrep
+ pkgs.taplo
+ pkgs.zensical
+ ];
+}
diff --git a/contrib/nix/mods/python.nix b/contrib/nix/mods/python.nix
new file mode 100644
index 00000000..5bd5ae25
--- /dev/null
+++ b/contrib/nix/mods/python.nix
@@ -0,0 +1,70 @@
+# Packages sourced from PyPI against lockfile imported with uv2nix
+
+{
+ pkgs,
+ lib,
+ uv2nix,
+ pyproject-nix,
+ pyproject-build-systems,
+ workspaceRoot,
+ python,
+}:
+
+let
+ workspace = uv2nix.lib.workspace.loadWorkspace { inherit workspaceRoot; };
+
+ # Prefer binary distributions (i.e. wheels) when we can, saves us some build
+ # complexity.
+ overlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; };
+
+ overrides = final: prev: {
+ # rjsmin doesn't publish wheels for macOS. uv.lock does not track build
+ # dependencies (see astral-sh/uv#5190), so we manually define 'setuptools'.
+ rjsmin = prev.rjsmin.overrideAttrs (old: {
+ nativeBuildInputs =
+ (old.nativeBuildInputs or [ ]) ++ final.resolveBuildSystem { setuptools = [ ]; };
+ });
+
+ # Narrow rebuild triggers to relevant files to avoid pulling in the whole
+ # source tree and thrashing the cache for it.
+ dash-base-sdk = prev.dash-base-sdk.overrideAttrs (_: {
+ src = lib.fileset.toSource {
+ root = workspaceRoot;
+ fileset = lib.fileset.unions [
+ (workspaceRoot + "/pyproject.toml")
+ (workspaceRoot + "/uv.lock")
+ ];
+ };
+ });
+ };
+
+ pythonSet = (pkgs.callPackage pyproject-nix.build.packages { inherit python; }).overrideScope (
+ lib.composeManyExtensions [
+ pyproject-build-systems.overlays.default
+ overlay
+ overrides
+ ]
+ );
+
+ # '.dev' is a union of '.lib' and '.tools', '.tools' is sourced from nixpkgs.
+ # Sourcing '.lib' satisfies '.dev', completing the dependency list.
+ venv = pythonSet.mkVirtualEnv "dash-base-sdk-lib" { dash-base-sdk = [ "lib" ]; };
+in
+{
+ packages = [
+ venv
+ pkgs.uv
+ ];
+
+ env = {
+ UV_PYTHON = "${venv}/bin/python";
+ UV_NO_SYNC = "1";
+ };
+
+ # `semgrep` and other Python applications place their own interpreter
+ # in PATH, eclipsing our interpreter, preventing it from importing our
+ # workspaces packages.
+ shellHook = ''
+ export PATH="${venv}/bin:$PATH"
+ '';
+}
diff --git a/contrib/nix/mods/rust.nix b/contrib/nix/mods/rust.nix
new file mode 100644
index 00000000..cd2e4433
--- /dev/null
+++ b/contrib/nix/mods/rust.nix
@@ -0,0 +1,11 @@
+# Rust toolchain pinned from rust-toolchain.toml
+
+{ pkgs, toolchainFile }:
+
+{
+ packages = [ (pkgs.rust-bin.fromRustupToolchainFile toolchainFile) ];
+
+ env = {
+ CARGO_TERM_COLOR = "always";
+ };
+}
diff --git a/contrib/nix/shell/ci.nix b/contrib/nix/shell/ci.nix
new file mode 100644
index 00000000..997f2490
--- /dev/null
+++ b/contrib/nix/shell/ci.nix
@@ -0,0 +1,9 @@
+# Development shell for continuous integration
+
+{ compose, mods, ... }:
+
+compose [
+ mods.nixpkgs
+ mods.python
+ mods.rust
+]
diff --git a/contrib/nix/shell/common.nix b/contrib/nix/shell/common.nix
new file mode 100644
index 00000000..4f6ad109
--- /dev/null
+++ b/contrib/nix/shell/common.nix
@@ -0,0 +1,48 @@
+# Common logic shared between development shells
+
+{
+ pkgs,
+ lib,
+ inputs,
+ root,
+}:
+
+let
+ # Folds modules into mkShell arguments. Conflicting variables will throw
+ # instead of allowing order-sensitive assignment.
+ compose =
+ mods:
+ let
+ envs = map (m: m.env or { }) mods;
+ names = lib.concatMap lib.attrNames envs;
+ clashes = lib.unique (lib.filter (n: lib.count (m: m == n) names > 1) names);
+ in
+ if clashes != [ ] then
+ throw "variables redefined: ${lib.concatStringsSep ", " clashes}"
+ else
+ pkgs.mkShell (
+ { packages = lib.concatMap (m: m.packages or [ ]) mods; } // lib.foldl' (a: b: a // b) { } envs
+ );
+in
+{
+ inherit
+ pkgs
+ lib
+ compose
+ ;
+
+ mods = {
+ nixpkgs = import ../mods/nixpkgs.nix { inherit pkgs; };
+ python = import ../mods/python.nix {
+ inherit pkgs lib;
+ inherit (inputs) uv2nix pyproject-nix pyproject-build-systems;
+ workspaceRoot = root;
+ # Must match `project.requires-python` in pyproject.toml, effective floor.
+ python = pkgs.python311;
+ };
+ rust = import ../mods/rust.nix {
+ inherit pkgs;
+ toolchainFile = root + "/rust-toolchain.toml";
+ };
+ };
+}
diff --git a/docs/dev/devshells.md b/docs/dev/devshells.md
new file mode 100644
index 00000000..58d14916
--- /dev/null
+++ b/docs/dev/devshells.md
@@ -0,0 +1,32 @@
+# Development Shells
+
+
+
+
+
+### Quirks
+
+
+
+* **The Python environment is read-only**
+
+ Development shells (devshells) are immutable, this extends to packages sourced from PyPI. `uv` will neither sync nor
+ install packages outside the initially defined set. To modify packages, edit [`pyproject.toml`](../../pyproject.toml),
+ then generate an updated lockfile by running `uv lock` in the shell. Then re-enter a fresh devshell, it should take on
+ the new definitions.
+
+* **Pinned versions of tools don't match against manual setup**
+
+ Manual setup installs PyPI-sourced dependencies with `.dev`. Not every PyPI package is written in Python nor does it
+ have to expose a Pythonic API to qualify for publication on PyPI. This allows PyPI to serve as a general means to
+ distribute binaries so long as they otherwise meet PyPI's guidelines.
+
+ This makes PyPI serve as a parallel package source and `.dev` leverages this to have better control over dependencies
+ instead of relying on platform-specific package sources. We do not do this in devshells, preferring `nixpkgs` when
+ feasible (and falling back on PyPI when it isn't, segmenting these packages as `.lib` in
+ [`pyproject.toml`](../../pyproject.toml)). This leads to predictable drift between the versions pinned in
+ [`uv.lock`](../../uv.lock) and versions published in the pinned snapshot of `nixpkgs` in
+ [`flake.lock`](../../contrib/nix/flake.lock).
+
+ This drift is benign. Should there be any difference in outcome, consider
+ [filing an issue](https://github.com/dashpay/base-sdk/issues/new).
diff --git a/docs/dev/getting_started.md b/docs/dev/getting_started.md
index ca260d6f..a4f79190 100644
--- a/docs/dev/getting_started.md
+++ b/docs/dev/getting_started.md
@@ -1,5 +1,10 @@
# Getting Started
+> [!TIP]
+> On ARM64 and AMD64 Linux and macOS hosts, we offer [development shells](./devshells.md) that provide environments
+> identical to or based on environments used in CI as an alternative to manual setup. The following instructions are
+> for manual setup.
+
## Installing Rust
> [!WARNING]
diff --git a/docs/zensical.toml b/docs/zensical.toml
index 59f2d8a2..c797cfde 100644
--- a/docs/zensical.toml
+++ b/docs/zensical.toml
@@ -18,6 +18,7 @@ nav = [
{ "Home" = "README.md" },
{ "Contributing" = [
{ "Getting Started" = "dev/getting_started.md" },
+ { "Development Shells" = "dev/devshells.md" },
{ "Documentation" = "dev/about_docs.md" },
{ "Maintenance" = "dev/maintenance.md" },
{ "Style Guide (Rust)" = "dev/guide_rust.md" },
diff --git a/maint/README.md b/maint/README.md
index 6133d5f4..753b3818 100644
--- a/maint/README.md
+++ b/maint/README.md
@@ -18,6 +18,7 @@ use [`lint_all.py`](./lint_all.py).
| [`lint_codeql.py`](./lint/lint_codeql.py) | Query Rust sources against [`maint/codeql/rust/*.ql`](./codeql/rust) | `check`, `apply`, `apply-all`, `run`, `run-all` | `codeql`, `rustc` |
| [`lint_javascript.py`](./lint/lint_javascript.py) | Lint Javascript sources against [`eslint.config.mjs`](js/eslint.config.mjs) | *None* | `npx` (part of Node.js), `eslint` (auto-retrieved by script) |
| [`lint_markdown.py`](./lint/lint_markdown.py) | Lint Markdown [documentation](../docs/dev/about_docs.md) | *None* | `pymarkdownlnt` |
+| [`lint_nix.py`](./lint/lint_nix.py) | Lint Nix sources against `nixfmt`'s RFC 166 style | `check`, `apply`, `apply-all` | `nixfmt`, `git` |
| [`lint_python.py`](./lint/lint_python.py) | Lint Python sources against `[tool.ruff]` options in [`pyproject.toml`](../pyproject.toml) | *None* | `ruff` |
| [`lint_rust.py`](./lint/lint_rust.py) | Lint Rust sources against [`rustfmt.toml`](../rustfmt.toml) | *None* | `cargo`, `rustfmt` |
| [`lint_semgrep.py`](./lint/lint_semgrep.py) | Lint source code against [`maint/semgrep`](./semgrep/rust) definitions | *None* | `semgrep` |
diff --git a/maint/lint/lint_nix.py b/maint/lint/lint_nix.py
new file mode 100755
index 00000000..a40f1356
--- /dev/null
+++ b/maint/lint/lint_nix.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# coding: latin-1
+
+#
+# Copyright (c) 2026-present, The Dash Core developers
+# SPDX-License-Identifier: MIT
+# See the accompanying file LICENSE or https://opensource.org/license/MIT
+#
+
+"""Check (and apply) RFC 166 styling for Nix definitions."""
+
+from __future__ import annotations
+
+import subprocess
+import sys
+from pathlib import Path
+
+from common import (
+ RETCODE_ERR,
+ RETCODE_SKIP,
+ declare_verbs,
+ format_verbs,
+ formatted,
+ is_plain_file,
+ require_bin,
+ root_dir,
+ touched,
+)
+
+SCRIPT = Path(__file__).stem
+
+
+def _sources(repo_root: Path, only: list[str] | None) -> list[Path]:
+ """Return tracked source files to lint or just *only* when given."""
+ if only is not None:
+ return [repo_root / name for name in only]
+ git = require_bin("git")
+ listed = subprocess.run( # noqa: S603
+ [git, "ls-files", "*.nix"],
+ capture_output=True,
+ check=True,
+ cwd=str(repo_root),
+ text=True,
+ )
+ return [
+ repo_root / name
+ for name in listed.stdout.splitlines()
+ if is_plain_file(repo_root, name)
+ ]
+
+
+def main() -> int:
+ args = declare_verbs(__doc__ or "", format_verbs("Nix file")).parse_args()
+
+ try:
+ nixfmt_bin = require_bin("nixfmt")
+ except FileNotFoundError as e:
+ print(f"{e}, skipping", file=sys.stderr)
+ return RETCODE_SKIP
+
+ repo_root = root_dir()
+ fix = args.verb.startswith("apply")
+ only = touched(repo_root, (".nix",)) if args.verb == "apply" else None
+ sources = _sources(repo_root, only)
+
+ return formatted(
+ SCRIPT,
+ "Nix file",
+ sources,
+ lambda paths: [
+ nixfmt_bin,
+ *([] if fix else ["--check"]),
+ *[str(p) for p in paths],
+ ],
+ fix=fix,
+ scoped=only is not None,
+ cwd=repo_root,
+ )
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except Exception as exc: # noqa: BLE001
+ print(exc, file=sys.stderr)
+ sys.exit(RETCODE_ERR)
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
index 2de0c03a..71332135 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,4 +1,4 @@
[toolchain]
channel = "nightly-2026-02-01"
-components = ["clippy", "rust-analyzer", "rustfmt"]
-profile = "default"
+components = ["clippy", "llvm-tools", "rust-analyzer", "rustfmt"]
+profile = "minimal"
From b11205805a4a0926b4120ca3e401b88186374b36 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 04:13:43 +0530
Subject: [PATCH 06/17] sdk%build(nix): specify `wasm32-unknown-unknown` for
Zensical builds
---
contrib/nix/mods/rust.nix | 10 ++++++++--
contrib/nix/shell/common.nix | 4 ++++
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/contrib/nix/mods/rust.nix b/contrib/nix/mods/rust.nix
index cd2e4433..1dcacdab 100644
--- a/contrib/nix/mods/rust.nix
+++ b/contrib/nix/mods/rust.nix
@@ -1,9 +1,15 @@
# Rust toolchain pinned from rust-toolchain.toml
-{ pkgs, toolchainFile }:
+{
+ pkgs,
+ toolchainFile,
+ targets,
+}:
{
- packages = [ (pkgs.rust-bin.fromRustupToolchainFile toolchainFile) ];
+ packages = [
+ ((pkgs.rust-bin.fromRustupToolchainFile toolchainFile).override { inherit targets; })
+ ];
env = {
CARGO_TERM_COLOR = "always";
diff --git a/contrib/nix/shell/common.nix b/contrib/nix/shell/common.nix
index 4f6ad109..5eb180e8 100644
--- a/contrib/nix/shell/common.nix
+++ b/contrib/nix/shell/common.nix
@@ -8,6 +8,9 @@
}:
let
+ # Target platform for web demos bundled with documentation.
+ commonTargets = [ "wasm32-unknown-unknown" ];
+
# Folds modules into mkShell arguments. Conflicting variables will throw
# instead of allowing order-sensitive assignment.
compose =
@@ -43,6 +46,7 @@ in
rust = import ../mods/rust.nix {
inherit pkgs;
toolchainFile = root + "/rust-toolchain.toml";
+ targets = commonTargets;
};
};
}
From f0707f71067c2f1318a380c4acd794e45caa2e69 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 12:49:07 +0530
Subject: [PATCH 07/17] sdk%feat(nix): carry the devshell in a container for
hosts without Nix
---
contrib/docker/Dockerfile | 24 ++++++++++++
contrib/docker/README.md | 61 +++++++++++++++++++++++++++++++
contrib/docker/daemon | 9 +++++
contrib/docker/docker-compose.yml | 56 ++++++++++++++++++++++++++++
contrib/docker/entrypoint | 32 ++++++++++++++++
contrib/docker/nix.conf | 8 ++++
contrib/nix/README.md | 8 ++++
docs/dev/devshells.md | 28 +++++++++++++-
8 files changed, 224 insertions(+), 2 deletions(-)
create mode 100644 contrib/docker/Dockerfile
create mode 100644 contrib/docker/README.md
create mode 100755 contrib/docker/daemon
create mode 100644 contrib/docker/docker-compose.yml
create mode 100755 contrib/docker/entrypoint
create mode 100644 contrib/docker/nix.conf
diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile
new file mode 100644
index 00000000..147e5573
--- /dev/null
+++ b/contrib/docker/Dockerfile
@@ -0,0 +1,24 @@
+FROM nixos/nix:2.35.2
+
+SHELL ["/bin/sh", "-c"]
+
+ENV PATH="/usr/local/bin:$PATH"
+
+RUN rm /etc/nix/nix.conf
+
+COPY ./nix.conf /etc/nix/nix.conf
+COPY ./daemon /usr/local/bin/daemon
+COPY ./entrypoint /usr/local/bin/entrypoint
+
+RUN chmod +x /usr/local/bin/daemon /usr/local/bin/entrypoint
+
+ARG HOST_UID=1000
+ARG HOST_GID=1000
+
+RUN printf 'nixuser:x:%s:\n' "${HOST_GID}" >> /etc/group \
+ && printf 'nixuser:x:%s:%s:Nix user:/var/cache/base-sdk/home:/bin/sh\n' \
+ "${HOST_UID}" "${HOST_GID}" >> /etc/passwd
+
+WORKDIR "/src/base-sdk"
+
+ENTRYPOINT ["/usr/local/bin/entrypoint"]
diff --git a/contrib/docker/README.md b/contrib/docker/README.md
new file mode 100644
index 00000000..3706874f
--- /dev/null
+++ b/contrib/docker/README.md
@@ -0,0 +1,61 @@
+## Docker
+
+> [!TIP]
+> On some platforms, `docker compose` is not included with the baseline Docker installation. In that case, you may
+> need to consult platform-specific guidance on installing Compose, like the
+> [`docker-compose`](https://packages.debian.org/trixie/docker-compose) package on Debian.
+
+To install Docker on your host, see [official guidance](https://docs.docker.com/get-started/get-docker/) for your
+platform. Note that unlike using Nix, the store used in Docker _cannot_ be shared with the host and using the provided
+containers is highly discouraged if you already use Nix on your host.
+
+**The Nix store is persisted as the volume `nix_store` and is expected to consume 15-20GB at a minimum, it is managed
+by the `nix_daemon` container and other Nix daemons must not compete for management of this store.**
+
+> [!WARNING]
+> The workspace is bind-mounted and the Compose project is incompatible with worktrees. If you are using paired
+> programming assistants like Claude Code or Codex, there is a fair chance worktrees are in use. Worktrees resolve
+> their parent repository by absolute path on the host, which isn't visible from the vantage point of the container.
+
+To build the image, from the [`contrib/docker`](.) directory, run
+
+```bash
+docker compose build
+```
+
+### Entering a shell
+
+> [!NOTE]
+> To prevent permissions issues, `HOST_UID` and `HOST_GID` are supplied to ensure that the container uses the same
+> UID:GID pair as the source code it is bind-mounted against. If undefined, they default to the default Linux pair,
+> `1000:1000`.
+
+To enter an interactive shell, from the [`contrib/docker`](.) directory, run
+
+```bash
+# Starts the containers and drops you into an interactive shell, reaped on exit. The daemon is left running
+HOST_UID=$(id -u) HOST_GID=$(id -g) docker compose run --rm nix_shell
+```
+
+To shut down the daemon, from the [`contrib/docker`](.) directory, run
+
+```bash
+docker compose down
+```
+
+### One-shot commands
+
+To execute a command _without_ switching to a shell; or for scripting, from the [`contrib/docker`](.) directory, run
+
+```bash
+docker compose run --rm nix_shell cargo build --workspace
+```
+
+### Reaping the Nix store
+
+To get rid of the persistent store and the build cache, freeing the associated space, from the
+[`contrib/docker`](.) directory, run
+
+```bash
+docker compose down --volumes
+```
diff --git a/contrib/docker/daemon b/contrib/docker/daemon
new file mode 100755
index 00000000..e4f96093
--- /dev/null
+++ b/contrib/docker/daemon
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+install -d -m 0755 -o "${HOST_UID}" -g "${HOST_GID}" \
+ /var/cache/base-sdk \
+ /var/cache/base-sdk/home
+
+exec nix-daemon
diff --git a/contrib/docker/docker-compose.yml b/contrib/docker/docker-compose.yml
new file mode 100644
index 00000000..50bd9b97
--- /dev/null
+++ b/contrib/docker/docker-compose.yml
@@ -0,0 +1,56 @@
+name: "base-sdk"
+
+services:
+ nix_daemon:
+ build:
+ context: "."
+ dockerfile: "./Dockerfile"
+ args:
+ HOST_UID: "${HOST_UID:-1000}"
+ HOST_GID: "${HOST_GID:-1000}"
+ entrypoint: ["/usr/local/bin/daemon"]
+ environment:
+ HOST_UID: "${HOST_UID:-1000}"
+ HOST_GID: "${HOST_GID:-1000}"
+ init: true
+ healthcheck:
+ test: ["CMD-SHELL", "test -S /nix/var/nix/daemon-socket/socket"]
+ interval: "1s"
+ timeout: "2s"
+ retries: 10
+ restart: "unless-stopped"
+ volumes:
+ - "build_cache:/var/cache/base-sdk"
+ - "nix_store:/nix"
+
+ nix_shell:
+ build:
+ context: "."
+ dockerfile: "./Dockerfile"
+ args:
+ HOST_UID: "${HOST_UID:-1000}"
+ HOST_GID: "${HOST_GID:-1000}"
+ depends_on:
+ nix_daemon:
+ condition: "service_healthy"
+ init: true
+ user: "${HOST_UID:-1000}:${HOST_GID:-1000}"
+ environment:
+ NIX_REMOTE: "daemon"
+ HOME: "/var/cache/base-sdk/home"
+ USER: "nixuser"
+ WORKSPACE_PATH: "/src/base-sdk"
+ stdin_open: true # Equivalent to -i
+ tty: true # Equivalent to -t
+ volumes:
+ - "build_cache:/var/cache/base-sdk"
+ - "nix_store:/nix"
+ - type: bind
+ source: "../.."
+ target: "/src/base-sdk"
+ bind:
+ create_host_path: false
+
+volumes:
+ nix_store:
+ build_cache:
diff --git a/contrib/docker/entrypoint b/contrib/docker/entrypoint
new file mode 100755
index 00000000..c04558a4
--- /dev/null
+++ b/contrib/docker/entrypoint
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+SOCKET="/nix/var/nix/daemon-socket/socket"
+if [[ ! -S "${SOCKET}" ]]; then
+ echo "${0##*/}: no nix daemon at ${SOCKET}, cannot continue!" >&2
+ exit 1
+fi
+
+: "${WORKSPACE_PATH:?no workspace to enter, expected a bind mount}"
+if ! git -C / config --global --fixed-value --get safe.directory "${WORKSPACE_PATH}" > /dev/null; then
+ git -C / config --global --add safe.directory "${WORKSPACE_PATH}"
+fi
+
+cd "${WORKSPACE_PATH}"
+
+if ! git rev-parse --git-dir > /dev/null 2>&1; then
+ if [[ -f .git ]]; then
+ gitdir="$(< .git)"
+ echo "${0##*/}: ${WORKSPACE_PATH} is a worktree of ${gitdir#gitdir: }, unsupported!" >&2
+ else
+ echo "${0##*/}: ${WORKSPACE_PATH} is not a git repository, cannot continue!" >&2
+ fi
+ exit 1
+fi
+
+if (( $# > 0 )); then
+ exec nix develop ./contrib/nix#ci --command "$@"
+else
+ exec nix develop ./contrib/nix#ci
+fi
diff --git a/contrib/docker/nix.conf b/contrib/docker/nix.conf
new file mode 100644
index 00000000..e2382037
--- /dev/null
+++ b/contrib/docker/nix.conf
@@ -0,0 +1,8 @@
+always-allow-substitutes = true
+bash-prompt-prefix = (nix:$name)\040
+build-users-group = nixbld
+experimental-features = nix-command flakes
+sandbox = false
+
+substituters = https://cache.nixos.org/ https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store
+trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
diff --git a/contrib/nix/README.md b/contrib/nix/README.md
index dd6d5a85..bc057677 100644
--- a/contrib/nix/README.md
+++ b/contrib/nix/README.md
@@ -1,3 +1,5 @@
+
+
## Nix
To maintain a consistent development environment and reproducible toolchain, a declarative environment is available
@@ -5,6 +7,10 @@ using [Nix](https://nixos.org) on macOS and Linux hosts on ARM64 and AMD64. **Wi
resort to using Nix through [Windows Subsystem for Linux](https://github.com/microsoft/WSL) or manually set up their
environment.**
+
+
+
+
### Setting up Nix
> [!WARNING]
@@ -38,3 +44,5 @@ To execute a command _without_ switching to a shell; or for scripting, use
```bash
nix develop ./contrib/nix#ci --command cargo test --workspace --features full
```
+
+
diff --git a/docs/dev/devshells.md b/docs/dev/devshells.md
index 58d14916..097d6297 100644
--- a/docs/dev/devshells.md
+++ b/docs/dev/devshells.md
@@ -1,12 +1,19 @@
# Development Shells
-
+
+
+> [!TIP]
+> Should you wish to utilize devshells without installing Nix on your host, the environment is also available wrapped in
+> a Docker [container](#docker); though it is still recommended to set up Nix for long-term development due to the cost
+> associated with maintaining a parallel Nix store.
+
+
### Quirks
-
+
* **The Python environment is read-only**
@@ -30,3 +37,20 @@
This drift is benign. Should there be any difference in outcome, consider
[filing an issue](https://github.com/dashpay/base-sdk/issues/new).
+
+
+
+
+
+### Quirks
+
+
+
+* **`nix_shell` doesn't work standalone**
+
+ `nix_shell` intentionally does not host the daemon, instead, delegating that to a dedicated `nix_daemon` container.
+ This is to avoid churn and to achieve better isolation. Nix stores are expensive in storage cost (and initially for
+ built elements, compute), so the interactive container talks to the store-hosting container, permitting flexible
+ setups where multiple containers can leverage the same underlying store.
+
+ Managing the containers _outside_ Docker Compose is unsupported.
From 9523592545ad549f88ed1bbd54caed2d9f560cf4 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Mon, 7 Sep 2026 16:51:15 +0530
Subject: [PATCH 08/17] sdk%feat(nix): define underlying compiler as Clang 20
---
contrib/nix/mods/cxx.nix | 11 +++++++++++
contrib/nix/mods/rust.nix | 15 ++++++++++++---
contrib/nix/shell/ci.nix | 1 +
contrib/nix/shell/common.nix | 18 ++++++++++++++----
4 files changed, 38 insertions(+), 7 deletions(-)
create mode 100644 contrib/nix/mods/cxx.nix
diff --git a/contrib/nix/mods/cxx.nix b/contrib/nix/mods/cxx.nix
new file mode 100644
index 00000000..8c74ab10
--- /dev/null
+++ b/contrib/nix/mods/cxx.nix
@@ -0,0 +1,11 @@
+# LLVM C(++) compiler setup and configuration
+
+{ pkgs, lib }:
+
+let
+ llvm = pkgs.llvmPackages_20;
+in
+{
+ packages = [ llvm.bintools ];
+ stdenv = llvm.stdenv;
+}
diff --git a/contrib/nix/mods/rust.nix b/contrib/nix/mods/rust.nix
index 1dcacdab..64420e05 100644
--- a/contrib/nix/mods/rust.nix
+++ b/contrib/nix/mods/rust.nix
@@ -6,10 +6,19 @@
targets,
}:
+let
+ # Purge C compiler wrapper propagated by rust-overlay to prioritize
+ # stdenv's C compiler (defined in cxx.nix)
+ toolchain =
+ ((pkgs.rust-bin.fromRustupToolchainFile toolchainFile).override { inherit targets; }).overrideAttrs
+ (_: {
+ propagatedBuildInputs = [ ];
+ depsHostHostPropagated = [ ];
+ depsTargetTargetPropagated = [ ];
+ });
+in
{
- packages = [
- ((pkgs.rust-bin.fromRustupToolchainFile toolchainFile).override { inherit targets; })
- ];
+ packages = [ toolchain ];
env = {
CARGO_TERM_COLOR = "always";
diff --git a/contrib/nix/shell/ci.nix b/contrib/nix/shell/ci.nix
index 997f2490..44a357d4 100644
--- a/contrib/nix/shell/ci.nix
+++ b/contrib/nix/shell/ci.nix
@@ -3,6 +3,7 @@
{ compose, mods, ... }:
compose [
+ mods.cxx
mods.nixpkgs
mods.python
mods.rust
diff --git a/contrib/nix/shell/common.nix b/contrib/nix/shell/common.nix
index 5eb180e8..9ec18726 100644
--- a/contrib/nix/shell/common.nix
+++ b/contrib/nix/shell/common.nix
@@ -11,19 +11,28 @@ let
# Target platform for web demos bundled with documentation.
commonTargets = [ "wasm32-unknown-unknown" ];
- # Folds modules into mkShell arguments. Conflicting variables will throw
- # instead of allowing order-sensitive assignment.
+ # Folds modules into mkShell arguments. Conflicting variables or stdenvs
+ # will throw instead of allowing order-sensitive assignment.
compose =
mods:
let
+ named = m: m._name or "";
envs = map (m: m.env or { }) mods;
names = lib.concatMap lib.attrNames envs;
clashes = lib.unique (lib.filter (n: lib.count (m: m == n) names > 1) names);
+ chosen = lib.filter (m: (m.stdenv or null) != null) mods;
+ mkShell =
+ if chosen == [ ] then
+ pkgs.mkShell
+ else if lib.length chosen == 1 then
+ pkgs.mkShell.override { stdenv = (lib.head chosen).stdenv; }
+ else
+ throw "stdenv redefined: ${lib.concatMapStringsSep ", " named chosen}";
in
if clashes != [ ] then
throw "variables redefined: ${lib.concatStringsSep ", " clashes}"
else
- pkgs.mkShell (
+ mkShell (
{ packages = lib.concatMap (m: m.packages or [ ]) mods; } // lib.foldl' (a: b: a // b) { } envs
);
in
@@ -34,7 +43,8 @@ in
compose
;
- mods = {
+ mods = lib.mapAttrs (name: m: m // { _name = name; }) {
+ cxx = import ../mods/cxx.nix { inherit pkgs lib; };
nixpkgs = import ../mods/nixpkgs.nix { inherit pkgs; };
python = import ../mods/python.nix {
inherit pkgs lib;
From a18c8d58102abb75f1e457022d3aca05f5141742 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Mon, 7 Sep 2026 17:37:08 +0530
Subject: [PATCH 09/17] sdk%feat(nix): add CodeQL with x86_64 emulation for
ARM64 Linux
---
contrib/nix/flake.nix | 5 ++
contrib/nix/mods/codeql.nix | 102 +++++++++++++++++++++++++++++++++++
contrib/nix/shell/ci.nix | 1 +
contrib/nix/shell/common.nix | 1 +
rust-toolchain.toml | 2 +-
5 files changed, 110 insertions(+), 1 deletion(-)
create mode 100644 contrib/nix/mods/codeql.nix
diff --git a/contrib/nix/flake.nix b/contrib/nix/flake.nix
index a494422e..16a4204e 100644
--- a/contrib/nix/flake.nix
+++ b/contrib/nix/flake.nix
@@ -53,6 +53,11 @@
f (
import nixpkgs {
inherit system;
+ config.allowUnfreePredicate =
+ pkg:
+ builtins.elem (lib.getName pkg) [
+ "codeql"
+ ];
overlays = [ rust-overlay.overlays.default ];
}
)
diff --git a/contrib/nix/mods/codeql.nix b/contrib/nix/mods/codeql.nix
new file mode 100644
index 00000000..41502225
--- /dev/null
+++ b/contrib/nix/mods/codeql.nix
@@ -0,0 +1,102 @@
+# CodeQL CLI
+
+{ pkgs, lib }:
+
+let
+ version = "2.26.1";
+ linux64 = {
+ file = "codeql-linux64.zip";
+ hash = "sha256-FUgN2m4gM2qcfdy2Fx4OkVXx/nOh0xuurBU4IcuJrqs=";
+ };
+ osx64 = {
+ file = "codeql-osx64.zip";
+ hash = "sha256-YcXStT4c2O4r1XwxpVxXr1P/qv3xnEbSNBcExsrPNdM=";
+ };
+
+ # CodeQL does not offer ARM64 builds for Linux (github/codeql#20616), so we
+ # resort to x86_64 emulation instead. macOS releases include both AMD64 and
+ # ARM64 support, mitigating the need for emulation.
+ targets = {
+ x86_64-linux = {
+ asset = linux64;
+ dir = "linux64";
+ emulate = false;
+ };
+ aarch64-linux = {
+ asset = linux64;
+ dir = "linux64";
+ emulate = true;
+ };
+ x86_64-darwin = {
+ asset = osx64;
+ dir = "osx64";
+ emulate = false;
+ };
+ aarch64-darwin = {
+ asset = osx64;
+ dir = "osx64";
+ emulate = false;
+ };
+ };
+
+ target = targets.${pkgs.stdenv.hostPlatform.system};
+
+ # The Linux archive names its tracer lib64trace.so and bundles an x86_64 JDK
+ # that will not run from a Nix store.
+ linuxFixup = ''
+ ln -sf $out/codeql/tools/linux64/lib64trace.so $out/codeql/tools/linux64/libtrace.so
+ rm -rf $out/codeql/tools/linux64/java
+ ln -s ${pkgs.zulu17} $out/codeql/tools/linux64/java
+ '';
+
+ # Wrapping all executables around QEMU to achieve x86_64 emulation. Shared
+ # objects are unmodified to avoid caller dlopen() breakage.
+ emulateFixup = ''
+ find $out/codeql -type f -perm -u+x -print0 |
+ while IFS= read -r -d "" bin; do
+ case "$(file -b "$bin")" in
+ *ELF*executable*x86-64*)
+ mv "$bin" "$bin.x86_64"
+ cat > "$bin" <
Date: Mon, 7 Sep 2026 17:48:55 +0530
Subject: [PATCH 10/17] sdk%feat(ci): replace MSRV, `nightly` and Zensical
workflows with Nix
---
.github/workflows/build_msrv.yml | 78 +++++++---------------------
.github/workflows/build_nightly.yml | 17 +++---
.github/workflows/pages.yml | 38 ++++----------
.github/workflows/pkg_num.yml | 4 ++
.github/workflows/pkg_p2p_core.yml | 4 ++
.github/workflows/pkg_params.yml | 4 ++
.github/workflows/pkg_pkc.yml | 4 ++
.github/workflows/pkg_pow.yml | 4 ++
.github/workflows/pkg_primitives.yml | 4 ++
.github/workflows/pkg_script.yml | 4 ++
.github/workflows/pkg_types.yml | 4 ++
contrib/nix/mods/nixpkgs.nix | 2 +
contrib/nix/mods/rust.nix | 32 +++++++-----
contrib/nix/shell/common.nix | 20 +++++--
docs/build_docs.py | 2 +-
docs/dev/about_docs.md | 1 +
16 files changed, 109 insertions(+), 113 deletions(-)
diff --git a/.github/workflows/build_msrv.yml b/.github/workflows/build_msrv.yml
index c09c35f5..c5544505 100644
--- a/.github/workflows/build_msrv.yml
+++ b/.github/workflows/build_msrv.yml
@@ -17,6 +17,10 @@ jobs:
name: Lint
runs-on: ubuntu-24.04
+ defaults:
+ run:
+ shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0}
+
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -34,42 +38,8 @@ jobs:
exit 1
fi
- - name: Install Rust toolchain
- uses: dtolnay/rust-toolchain@1.85.0
- with:
- components: rustfmt
-
- - name: Set up Node.js
- uses: actions/setup-node@v5
- with:
- node-version: 24
-
- - name: Set up Python
- id: python
- uses: actions/setup-python@v6
- with:
- python-version-file: pyproject.toml
-
- - name: Set up uv
- uses: astral-sh/setup-uv@v10.0.1
- with:
- version: 0.12.9
- enable-cache: true
- cache-dependency-glob: uv.lock
-
- - name: Install Python dependencies
- run: |
- uv sync --locked --extra dev --python '${{ steps.python.outputs.python-path }}'
- echo "${PWD}/.venv/bin" >> "${GITHUB_PATH}"
-
- - name: Install CodeQL
- id: setup-codeql
- uses: github/codeql-action/setup-codeql@v4.37.1
- with:
- tools: linked
-
- - name: Setup CodeQL
- run: echo "$(dirname '${{ steps.setup-codeql.outputs.codeql-path }}')" >> "$GITHUB_PATH"
+ - name: Install Nix
+ uses: DeterminateSystems/nix-installer-action@v22
- name: Restore cargo registry
uses: actions/cache/restore@v5
@@ -80,14 +50,6 @@ jobs:
key: cargo-deps-${{ hashFiles('Cargo.lock', 'docs/samples/Cargo.lock') }}
restore-keys: cargo-deps-
- - name: Restore build artifacts
- uses: actions/cache/restore@v5
- with:
- path: target
- key: cargo-build-msrv-${{ runner.os }}-${{ runner.arch }}-${{ github.sha }}
- restore-keys: |
- cargo-build-msrv-${{ runner.os }}-${{ runner.arch }}-
-
- name: Manage CodeQL packs
uses: actions/cache@v5
with:
@@ -98,13 +60,9 @@ jobs:
run: |
python3 maint/lint_all.py --exclude lint_codeql
python3 maint/lint/lint_codeql.py check
- env:
- RUSTUP_TOOLCHAIN: 1.85.0
- name: Run CodeQL
run: python3 maint/lint/lint_codeql.py run --lang=rust --with-suite=rust-security-and-quality
- env:
- RUSTUP_TOOLCHAIN: 1.85.0
- name: Check PR commit messages
if: github.event_name == 'pull_request'
@@ -116,6 +74,10 @@ jobs:
name: Build and test
runs-on: ubuntu-24.04-arm
+ defaults:
+ run:
+ shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0}
+
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -133,8 +95,8 @@ jobs:
exit 1
fi
- - name: Install Rust toolchain
- uses: dtolnay/rust-toolchain@1.85.0
+ - name: Install Nix
+ uses: DeterminateSystems/nix-installer-action@v22
- name: Restore cargo registry
uses: actions/cache/restore@v5
@@ -155,14 +117,14 @@ jobs:
- name: Build workspace
run: |
- cargo build --workspace --exclude dash-pow --features full
- cargo build -p dash-pow --features std,aes_hw
- env:
- RUSTUP_TOOLCHAIN: 1.85.0
+ export RUSTC="$TOOLCHAIN_MSRV/bin/rustc"
+ export RUSTDOC="$TOOLCHAIN_MSRV/bin/rustdoc"
+ "$TOOLCHAIN_MSRV/bin/cargo" build --workspace --exclude dash-pow --features full
+ "$TOOLCHAIN_MSRV/bin/cargo" build -p dash-pow --features std,aes_hw
- name: Test workspace
run: |
- cargo test --workspace --exclude dash-pow --features full
- cargo test -p dash-pow --features std,aes_hw
- env:
- RUSTUP_TOOLCHAIN: 1.85.0
+ export RUSTC="$TOOLCHAIN_MSRV/bin/rustc"
+ export RUSTDOC="$TOOLCHAIN_MSRV/bin/rustdoc"
+ "$TOOLCHAIN_MSRV/bin/cargo" test --workspace --exclude dash-pow --features full
+ "$TOOLCHAIN_MSRV/bin/cargo" test -p dash-pow --features std,aes_hw
diff --git a/.github/workflows/build_nightly.yml b/.github/workflows/build_nightly.yml
index 3d11fb3a..6e077321 100644
--- a/.github/workflows/build_nightly.yml
+++ b/.github/workflows/build_nightly.yml
@@ -32,6 +32,10 @@ jobs:
- { name: min, args: "--no-default-features" }
- { name: std, args: "--features std" }
+ defaults:
+ run:
+ shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0}
+
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -49,11 +53,8 @@ jobs:
exit 1
fi
- - name: Install Rust toolchain
- uses: dtolnay/rust-toolchain@master
- with:
- toolchain: nightly-2026-02-01
- components: clippy, llvm-tools, rustfmt
+ - name: Install Nix
+ uses: DeterminateSystems/nix-installer-action@v22
- name: Manage cargo registry
uses: actions/cache@v5
@@ -82,13 +83,9 @@ jobs:
if: matrix.config.name != 'full'
run: cargo test -p ${{ inputs.package }} ${{ matrix.config.args }}
- - name: Install cargo-llvm-cov
- if: matrix.config.name == 'full'
- uses: taiki-e/install-action@cargo-llvm-cov
-
- name: Check formatting
if: matrix.config.name == 'full'
- run: python maint/lint/lint_rust.py
+ run: python3 maint/lint/lint_rust.py
- name: Test package (with coverage)
if: matrix.config.name == 'full'
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 2afb1ea4..48cf3033 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -10,8 +10,10 @@ on:
- .github/workflows/pages.yml
- pyproject.toml
- uv.lock
+ - contrib/nix/**
+ - rust-toolchain.toml
- README.md
- - '**/README.md'
+ - "**/README.md"
workflow_dispatch:
concurrency:
@@ -26,6 +28,10 @@ jobs:
name: Build site
runs-on: ubuntu-24.04-arm
+ defaults:
+ run:
+ shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0}
+
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -33,32 +39,8 @@ jobs:
fetch-depth: 1
persist-credentials: false
- - name: Install Rust toolchain
- uses: dtolnay/rust-toolchain@master
- with:
- toolchain: nightly-2026-02-01
- targets: wasm32-unknown-unknown
-
- - name: Install wasm-pack
- run: cargo install wasm-pack@0.15.0
-
- - name: Set up Python
- id: python
- uses: actions/setup-python@v6
- with:
- python-version-file: pyproject.toml
-
- - name: Set up uv
- uses: astral-sh/setup-uv@v10.0.1
- with:
- version: 0.12.9
- enable-cache: true
- cache-dependency-glob: uv.lock
-
- - name: Install Python dependencies
- run: |
- uv sync --locked --extra dev --python '${{ steps.python.outputs.python-path }}'
- echo "${PWD}/.venv/bin" >> "${GITHUB_PATH}"
+ - name: Install Nix
+ uses: DeterminateSystems/nix-installer-action@v22
- name: Test documentation tooling
run: pytest
@@ -73,7 +55,7 @@ jobs:
restore-keys: cargo-deps-
- name: Build documentation
- run: python docs/build_docs.py build
+ run: python3 docs/build_docs.py build
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5
diff --git a/.github/workflows/pkg_num.yml b/.github/workflows/pkg_num.yml
index 2023c8c3..a399bc32 100644
--- a/.github/workflows/pkg_num.yml
+++ b/.github/workflows/pkg_num.yml
@@ -10,6 +10,10 @@ on:
- .github/workflows/pkg_num.yml
- .github/workflows/build_stable.yml
- .github/workflows/build_nightly.yml
+ - contrib/nix/**
+ - pyproject.toml
+ - rust-toolchain.toml
+ - uv.lock
pull_request:
paths: *paths
diff --git a/.github/workflows/pkg_p2p_core.yml b/.github/workflows/pkg_p2p_core.yml
index 8bda0071..dece3458 100644
--- a/.github/workflows/pkg_p2p_core.yml
+++ b/.github/workflows/pkg_p2p_core.yml
@@ -15,6 +15,10 @@ on:
- .github/workflows/pkg_p2p_core.yml
- .github/workflows/build_stable.yml
- .github/workflows/build_nightly.yml
+ - contrib/nix/**
+ - pyproject.toml
+ - rust-toolchain.toml
+ - uv.lock
pull_request:
paths: *paths
diff --git a/.github/workflows/pkg_params.yml b/.github/workflows/pkg_params.yml
index 47f5bd0c..9f9c5f6e 100644
--- a/.github/workflows/pkg_params.yml
+++ b/.github/workflows/pkg_params.yml
@@ -13,6 +13,10 @@ on:
- .github/workflows/pkg_params.yml
- .github/workflows/build_stable.yml
- .github/workflows/build_nightly.yml
+ - contrib/nix/**
+ - pyproject.toml
+ - rust-toolchain.toml
+ - uv.lock
pull_request:
paths: *paths
diff --git a/.github/workflows/pkg_pkc.yml b/.github/workflows/pkg_pkc.yml
index da71da1d..7d0cbb25 100644
--- a/.github/workflows/pkg_pkc.yml
+++ b/.github/workflows/pkg_pkc.yml
@@ -12,6 +12,10 @@ on:
- .github/workflows/pkg_pkc.yml
- .github/workflows/build_stable.yml
- .github/workflows/build_nightly.yml
+ - contrib/nix/**
+ - pyproject.toml
+ - rust-toolchain.toml
+ - uv.lock
pull_request:
paths: *paths
diff --git a/.github/workflows/pkg_pow.yml b/.github/workflows/pkg_pow.yml
index 5a95334c..26b1816d 100644
--- a/.github/workflows/pkg_pow.yml
+++ b/.github/workflows/pkg_pow.yml
@@ -11,6 +11,10 @@ on:
- .github/workflows/pkg_pow.yml
- .github/workflows/build_stable.yml
- .github/workflows/build_nightly.yml
+ - contrib/nix/**
+ - pyproject.toml
+ - rust-toolchain.toml
+ - uv.lock
pull_request:
paths: *paths
diff --git a/.github/workflows/pkg_primitives.yml b/.github/workflows/pkg_primitives.yml
index 56ecdb49..40fcaf99 100644
--- a/.github/workflows/pkg_primitives.yml
+++ b/.github/workflows/pkg_primitives.yml
@@ -13,6 +13,10 @@ on:
- .github/workflows/pkg_primitives.yml
- .github/workflows/build_stable.yml
- .github/workflows/build_nightly.yml
+ - contrib/nix/**
+ - pyproject.toml
+ - rust-toolchain.toml
+ - uv.lock
pull_request:
paths: *paths
diff --git a/.github/workflows/pkg_script.yml b/.github/workflows/pkg_script.yml
index 7b40c41e..f49edae5 100644
--- a/.github/workflows/pkg_script.yml
+++ b/.github/workflows/pkg_script.yml
@@ -11,6 +11,10 @@ on:
- .github/workflows/pkg_script.yml
- .github/workflows/build_stable.yml
- .github/workflows/build_nightly.yml
+ - contrib/nix/**
+ - pyproject.toml
+ - rust-toolchain.toml
+ - uv.lock
pull_request:
paths: *paths
diff --git a/.github/workflows/pkg_types.yml b/.github/workflows/pkg_types.yml
index 3b6f8cdb..e6d4fff1 100644
--- a/.github/workflows/pkg_types.yml
+++ b/.github/workflows/pkg_types.yml
@@ -10,6 +10,10 @@ on:
- .github/workflows/pkg_types.yml
- .github/workflows/build_stable.yml
- .github/workflows/build_nightly.yml
+ - contrib/nix/**
+ - pyproject.toml
+ - rust-toolchain.toml
+ - uv.lock
pull_request:
paths: *paths
diff --git a/contrib/nix/mods/nixpkgs.nix b/contrib/nix/mods/nixpkgs.nix
index c02b5b61..da9fd1f9 100644
--- a/contrib/nix/mods/nixpkgs.nix
+++ b/contrib/nix/mods/nixpkgs.nix
@@ -4,9 +4,11 @@
{
packages = [
+ pkgs.cargo-llvm-cov
pkgs.git
pkgs.nixfmt
pkgs.nodejs_24
+ pkgs.wasm-pack
# Packages synced with '.tools' from 'pyproject.toml'.
pkgs.ruff
diff --git a/contrib/nix/mods/rust.nix b/contrib/nix/mods/rust.nix
index 64420e05..66ae95a9 100644
--- a/contrib/nix/mods/rust.nix
+++ b/contrib/nix/mods/rust.nix
@@ -1,26 +1,34 @@
-# Rust toolchain pinned from rust-toolchain.toml
+# Rust toolchains, default included in PATH, remaining as TOOLCHAIN_*
{
+ default,
+ lib,
pkgs,
- toolchainFile,
- targets,
+ toolchains,
}:
let
# Purge C compiler wrapper propagated by rust-overlay to prioritize
# stdenv's C compiler (defined in cxx.nix)
- toolchain =
- ((pkgs.rust-bin.fromRustupToolchainFile toolchainFile).override { inherit targets; }).overrideAttrs
- (_: {
- propagatedBuildInputs = [ ];
- depsHostHostPropagated = [ ];
- depsTargetTargetPropagated = [ ];
- });
+ bare = lib.mapAttrs (
+ _: t:
+ t.overrideAttrs (_: {
+ propagatedBuildInputs = [ ];
+ depsHostHostPropagated = [ ];
+ depsTargetTargetPropagated = [ ];
+ })
+ ) toolchains;
+
+ # A path per non-default toolchain, since only one can own `cargo` at a time.
+ named = lib.mapAttrs' (name: t: lib.nameValuePair "TOOLCHAIN_${lib.toUpper name}" "${t}") (
+ lib.filterAttrs (name: _: name != default) bare
+ );
in
{
- packages = [ toolchain ];
+ packages = [ bare.${default} ];
env = {
CARGO_TERM_COLOR = "always";
- };
+ }
+ // named;
}
diff --git a/contrib/nix/shell/common.nix b/contrib/nix/shell/common.nix
index 3ac7f178..a3f34bb4 100644
--- a/contrib/nix/shell/common.nix
+++ b/contrib/nix/shell/common.nix
@@ -11,6 +11,8 @@ let
# Target platform for web demos bundled with documentation.
commonTargets = [ "wasm32-unknown-unknown" ];
+ rsComponents = (lib.importTOML (root + "/rust-toolchain.toml")).toolchain.components;
+
# Folds modules into mkShell arguments. Conflicting variables or stdenvs
# will throw instead of allowing order-sensitive assignment.
compose =
@@ -33,7 +35,11 @@ let
throw "variables redefined: ${lib.concatStringsSep ", " clashes}"
else
mkShell (
- { packages = lib.concatMap (m: m.packages or [ ]) mods; } // lib.foldl' (a: b: a // b) { } envs
+ {
+ packages = lib.concatMap (m: m.packages or [ ]) mods;
+ shellHook = lib.concatStringsSep "\n" (lib.filter (h: h != "") (map (m: m.shellHook or "") mods));
+ }
+ // lib.foldl' (a: b: a // b) { } envs
);
in
{
@@ -55,9 +61,15 @@ in
python = pkgs.python311;
};
rust = import ../mods/rust.nix {
- inherit pkgs;
- toolchainFile = root + "/rust-toolchain.toml";
- targets = commonTargets;
+ inherit pkgs lib;
+ default = "nightly";
+ toolchains = {
+ nightly = (pkgs.rust-bin.fromRustupToolchainFile (root + "/rust-toolchain.toml")).override {
+ targets = commonTargets;
+ };
+ # Must match `workspace.package.rust-version` in root Cargo.toml.
+ msrv = pkgs.rust-bin.stable."1.85.0".minimal.override { extensions = rsComponents; };
+ };
};
};
}
diff --git a/docs/build_docs.py b/docs/build_docs.py
index bc9a54d9..70b97f85 100755
--- a/docs/build_docs.py
+++ b/docs/build_docs.py
@@ -108,8 +108,8 @@ def _build_wasm_samples(root: Path, wasm_pack: str, cfg: Config) -> None:
"CARGO_TARGET_DIR": str(root / "target" / "samples"),
"CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS":
"-C target-feature=+simd128",
- "RUSTUP_TOOLCHAIN": channel,
}
+ env.setdefault("RUSTUP_TOOLCHAIN", channel)
for cargo_toml in samples:
crate_dir = cargo_toml.parent
diff --git a/docs/dev/about_docs.md b/docs/dev/about_docs.md
index 45514310..e50a7466 100644
--- a/docs/dev/about_docs.md
+++ b/docs/dev/about_docs.md
@@ -8,6 +8,7 @@ This guide is generated using [Zensical](https://pypi.org/project/zensical/) (a
> [!NOTE]
> If you haven't set up your development environment, check out the [startup guide](./getting_started.md) first.
+> If you're in a [development shell](./devshells.md), `wasm-pack` is included and this step can be skipped.
The documentation comes bundled with web-ready demos, which are powered by WebAssembly. Preparing them for
distribution relies on [`wasm-pack`](https://github.com/wasm-bindgen/wasm-pack), which is installed as a binary crate.
From bd4d0dbc8a745e2945e2ad85391058b6eec964df Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Mon, 7 Sep 2026 17:51:36 +0530
Subject: [PATCH 11/17] sdk%feat(ci): cache Nix artifacts using `cachix-action`
---
.github/workflows/build_msrv.yml | 22 ++++++++++++++++++++++
.github/workflows/build_nightly.yml | 13 +++++++++++++
.github/workflows/pages.yml | 11 +++++++++++
contrib/docker/nix.conf | 4 ++--
4 files changed, 48 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/build_msrv.yml b/.github/workflows/build_msrv.yml
index c5544505..d437b794 100644
--- a/.github/workflows/build_msrv.yml
+++ b/.github/workflows/build_msrv.yml
@@ -40,6 +40,17 @@ jobs:
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v22
+ with:
+ extra-conf: |
+ extra-substituters = https://dashpay-base-sdk.cachix.org
+ extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI=
+
+ - name: Cache Nix
+ uses: cachix/cachix-action@v17
+ with:
+ name: dashpay-base-sdk
+ authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
+ skipAddingSubstituter: true
- name: Restore cargo registry
uses: actions/cache/restore@v5
@@ -97,6 +108,17 @@ jobs:
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v22
+ with:
+ extra-conf: |
+ extra-substituters = https://dashpay-base-sdk.cachix.org
+ extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI=
+
+ - name: Cache Nix
+ uses: cachix/cachix-action@v17
+ with:
+ name: dashpay-base-sdk
+ authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
+ skipAddingSubstituter: true
- name: Restore cargo registry
uses: actions/cache/restore@v5
diff --git a/.github/workflows/build_nightly.yml b/.github/workflows/build_nightly.yml
index 6e077321..a9d3a8a9 100644
--- a/.github/workflows/build_nightly.yml
+++ b/.github/workflows/build_nightly.yml
@@ -12,6 +12,8 @@ on:
required: true
type: string
secrets:
+ CACHIX_AUTH_TOKEN:
+ required: false
CODECOV_TOKEN:
required: false
@@ -55,6 +57,17 @@ jobs:
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v22
+ with:
+ extra-conf: |
+ extra-substituters = https://dashpay-base-sdk.cachix.org
+ extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI=
+
+ - name: Cache Nix
+ uses: cachix/cachix-action@v17
+ with:
+ name: dashpay-base-sdk
+ authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
+ skipAddingSubstituter: true
- name: Manage cargo registry
uses: actions/cache@v5
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 48cf3033..362b53b6 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -41,6 +41,17 @@ jobs:
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v22
+ with:
+ extra-conf: |
+ extra-substituters = https://dashpay-base-sdk.cachix.org
+ extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI=
+
+ - name: Cache Nix
+ uses: cachix/cachix-action@v17
+ with:
+ name: dashpay-base-sdk
+ authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
+ skipAddingSubstituter: true
- name: Test documentation tooling
run: pytest
diff --git a/contrib/docker/nix.conf b/contrib/docker/nix.conf
index e2382037..9bc229a8 100644
--- a/contrib/docker/nix.conf
+++ b/contrib/docker/nix.conf
@@ -4,5 +4,5 @@ build-users-group = nixbld
experimental-features = nix-command flakes
sandbox = false
-substituters = https://cache.nixos.org/ https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store
-trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
+substituters = https://cache.nixos.org/ https://dashpay-base-sdk.cachix.org https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store
+trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI=
From 9fa6225471a7b4a9eff09ca7b180614298ec0475 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 03:27:19 +0530
Subject: [PATCH 12/17] sdk%feat(nix): add support for cross-compilation for
Linux
---
.github/workflows/build_cross.yml | 98 +++++++++++++++++++++++++
contrib/nix/mods/cxx.nix | 116 +++++++++++++++++++++++++++++-
contrib/nix/shell/ci.nix | 9 ++-
contrib/nix/shell/common.nix | 21 ++++--
contrib/nix/smoke_test.sh | 38 ++++++++++
docs/dev/cross_compilation.md | 49 +++++++++++++
docs/zensical.toml | 1 +
7 files changed, 325 insertions(+), 7 deletions(-)
create mode 100644 .github/workflows/build_cross.yml
create mode 100755 contrib/nix/smoke_test.sh
create mode 100644 docs/dev/cross_compilation.md
diff --git a/.github/workflows/build_cross.yml b/.github/workflows/build_cross.yml
new file mode 100644
index 00000000..e5932964
--- /dev/null
+++ b/.github/workflows/build_cross.yml
@@ -0,0 +1,98 @@
+name: Cross-compilation build
+
+on:
+ push:
+ branches: [develop]
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ shell:
+ name: Initialize devshell
+ runs-on: ubuntu-24.04-arm
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 1
+
+ - name: Install Nix
+ uses: DeterminateSystems/nix-installer-action@v22
+ with:
+ extra-conf: |
+ extra-substituters = https://dashpay-base-sdk.cachix.org
+ extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI=
+
+ - name: Cache Nix
+ uses: cachix/cachix-action@v17
+ with:
+ name: dashpay-base-sdk
+ authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
+ skipAddingSubstituter: true
+
+ - name: Initialize devshell
+ run: nix develop ./contrib/nix#ci --command true
+
+ - name: Validate toolchains
+ run: nix develop ./contrib/nix#ci --command ./contrib/nix/smoke_test.sh
+
+ build:
+ name: ${{ matrix.target }}
+ runs-on: ubuntu-24.04-arm
+ needs: shell
+ strategy:
+ fail-fast: false
+ matrix:
+ target:
+ - x86_64-unknown-linux-gnu
+
+ defaults:
+ run:
+ shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 1
+
+ - name: Install Nix
+ uses: DeterminateSystems/nix-installer-action@v22
+ with:
+ extra-conf: |
+ extra-substituters = https://dashpay-base-sdk.cachix.org
+ extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI=
+
+ - name: Cache Nix
+ uses: cachix/cachix-action@v17
+ with:
+ name: dashpay-base-sdk
+ authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
+ skipAddingSubstituter: true
+
+ - name: Restore cargo registry
+ uses: actions/cache/restore@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ key: cargo-deps-${{ hashFiles('Cargo.lock', 'docs/samples/Cargo.lock') }}
+ restore-keys: cargo-deps-
+
+ - name: Manage build artifacts
+ uses: actions/cache@v5
+ with:
+ path: target
+ key: cargo-build-cross-${{ matrix.target }}-${{ github.sha }}
+ restore-keys: |
+ cargo-build-cross-${{ matrix.target }}-
+
+ - name: Build workspace
+ run: cargo build --workspace --all-targets --features full --target ${{ matrix.target }}
diff --git a/contrib/nix/mods/cxx.nix b/contrib/nix/mods/cxx.nix
index 8c74ab10..6300d5d9 100644
--- a/contrib/nix/mods/cxx.nix
+++ b/contrib/nix/mods/cxx.nix
@@ -4,8 +4,120 @@
let
llvm = pkgs.llvmPackages_20;
+
+ # clang -print-resource-dir names the main output, but the builtin headers are
+ # split into .lib, so the flag has to be passed explicitly.
+ resourceDir = "${llvm.clang-unwrapped.lib}/lib/clang/20";
+
+ # --ld-path names lld directly, where -fuse-ld=lld would find the nixpkgs
+ # bintools wrapper on PATH. Mach-O needs ld64.
+ ldFor = flavour: "--ld-path=${llvm.lld}/bin/${flavour}";
+
+ # Per-target definitions keyed against `hostTriple`
+ defs = {
+ "aarch64-unknown-linux-gnu" = {
+ clangTarget = "aarch64-unknown-linux-gnu";
+ kind = "glibc";
+ cross = pkgs.pkgsCross.aarch64-multiplatform;
+ };
+ "x86_64-unknown-linux-gnu" = {
+ clangTarget = "x86_64-unknown-linux-gnu";
+ kind = "glibc";
+ cross = pkgs.pkgsCross.gnu64;
+ };
+ };
+
+ # glibc splits its outputs, so there isn't a unified tree to hand --sysroot
+ # headers are in .dev, crt objects and libraries are in .out. libgcc_s.so
+ # is a third output independent of the compiler's libraries.
+ glibcFlags =
+ d:
+ let
+ gccLib = "${d.cross.stdenv.cc.cc}/lib/gcc/${d.clangTarget}/${d.cross.stdenv.cc.cc.version}";
+ in
+ [
+ "-isystem ${d.cross.stdenv.cc.libc.dev}/include"
+ "-B${d.cross.stdenv.cc.libc.out}/lib"
+ "-B${gccLib}"
+ "-L${d.cross.stdenv.cc.libc.out}/lib"
+ "-L${gccLib}"
+ "-L${d.cross.stdenv.cc.cc.libgcc}/lib"
+ (ldFor "ld.lld")
+ ];
+
+ # libstdc++ comes from the cross GCC rather than from the sysroot
+ libStdCxx =
+ d:
+ let
+ cc = d.cross.stdenv.cc.cc;
+ inc = "${cc}/include/c++/${cc.version}";
+ in
+ [
+ "-isystem ${inc}"
+ "-isystem ${inc}/${d.clangTarget}"
+ "-L${cc}/${d.clangTarget}/lib"
+ ];
+
+ cxxExtra = d: { glibc = libStdCxx; }.${d.kind} d;
+ flagsFor = d: { glibc = glibcFlags; }.${d.kind} d;
+
+ # A driver per target and language. C_INCLUDE_PATH and CPLUS_INCLUDE_PATH
+ # are unset because the host's include paths would otherwise leak into a
+ # cross compile.
+ driver =
+ name: bin: d: extra:
+ pkgs.writeShellScriptBin name ''
+ exec env -u C_INCLUDE_PATH -u CPLUS_INCLUDE_PATH \
+ ${llvm.clang-unwrapped}/bin/${bin} \
+ --target=${d.clangTarget} \
+ -resource-dir=${resourceDir} \
+ ${lib.concatStringsSep " \\\n " (extra ++ flagsFor d)} \
+ "$@"
+ '';
+
+ # `cc-rs` and `cargo` spell the same target differently.
+ ccKey = t: builtins.replaceStrings [ "-" ] [ "_" ] t;
+ cargoKey = t: lib.toUpper (ccKey t);
+
+ wire =
+ t:
+ let
+ d = defs.${t} or (throw "cxx.nix knows no C toolchain for ${t}");
+ cc = driver "${t}-cc" "clang" d [ ];
+ cxx = driver "${t}-c++" "clang++" d (cxxExtra d);
+ in
+ {
+ packages = [
+ cc
+ cxx
+ ];
+ env = {
+ "CC_${ccKey t}" = "${cc}/bin/${t}-cc";
+ "CXX_${ccKey t}" = "${cxx}/bin/${t}-c++";
+ "AR_${ccKey t}" = "${llvm.bintools-unwrapped}/bin/llvm-ar";
+ "CARGO_TARGET_${cargoKey t}_LINKER" = "${cc}/bin/${t}-cc";
+ };
+ };
in
{
- packages = [ llvm.bintools ];
- stdenv = llvm.stdenv;
+ # Adding clang to packages would not displace the cc-wrapper the default
+ # stdenv puts on PATH, so the shell is built against this stdenv instead.
+ compiler = {
+ stdenv = llvm.stdenv;
+ packages = [ llvm.bintools ];
+ };
+
+ # attrNames does not force the values, so listing targets is cheap even
+ # though each entry reaches for a pkgsCross set.
+ knownTargets = builtins.attrNames defs;
+
+ forTargets =
+ targets:
+ let
+ wired = map wire targets;
+ in
+ {
+ packages = lib.concatMap (w: w.packages) wired;
+ env = lib.foldl' (a: w: a // w.env) { } wired;
+ };
}
diff --git a/contrib/nix/shell/ci.nix b/contrib/nix/shell/ci.nix
index 53857c59..28d704ff 100644
--- a/contrib/nix/shell/ci.nix
+++ b/contrib/nix/shell/ci.nix
@@ -1,8 +1,15 @@
# Development shell for continuous integration
-{ compose, mods, ... }:
+{
+ compose,
+ crossTargets,
+ cxx,
+ mods,
+ ...
+}:
compose [
+ (cxx.forTargets crossTargets)
mods.codeql
mods.cxx
mods.nixpkgs
diff --git a/contrib/nix/shell/common.nix b/contrib/nix/shell/common.nix
index a3f34bb4..56ad202d 100644
--- a/contrib/nix/shell/common.nix
+++ b/contrib/nix/shell/common.nix
@@ -11,8 +11,20 @@ let
# Target platform for web demos bundled with documentation.
commonTargets = [ "wasm32-unknown-unknown" ];
+ cxx = import ../mods/cxx.nix { inherit pkgs lib; };
+
rsComponents = (lib.importTOML (root + "/rust-toolchain.toml")).toolchain.components;
+ hostTriple = pkgs.stdenv.hostPlatform.rust.rustcTarget;
+ sameOs = t: lib.hasInfix (if pkgs.stdenv.hostPlatform.isDarwin then "apple-darwin" else "linux") t;
+ crossTargets = lib.filter (t: t != hostTriple && sameOs t) cxx.knownTargets;
+
+ nightlyWith =
+ extra:
+ (pkgs.rust-bin.fromRustupToolchainFile (root + "/rust-toolchain.toml")).override {
+ targets = commonTargets ++ extra;
+ };
+
# Folds modules into mkShell arguments. Conflicting variables or stdenvs
# will throw instead of allowing order-sensitive assignment.
compose =
@@ -47,11 +59,14 @@ in
pkgs
lib
compose
+ crossTargets
+ cxx
+ nightlyWith
;
mods = lib.mapAttrs (name: m: m // { _name = name; }) {
codeql = import ../mods/codeql.nix { inherit pkgs lib; };
- cxx = import ../mods/cxx.nix { inherit pkgs lib; };
+ cxx = cxx.compiler;
nixpkgs = import ../mods/nixpkgs.nix { inherit pkgs; };
python = import ../mods/python.nix {
inherit pkgs lib;
@@ -64,9 +79,7 @@ in
inherit pkgs lib;
default = "nightly";
toolchains = {
- nightly = (pkgs.rust-bin.fromRustupToolchainFile (root + "/rust-toolchain.toml")).override {
- targets = commonTargets;
- };
+ nightly = nightlyWith crossTargets;
# Must match `workspace.package.rust-version` in root Cargo.toml.
msrv = pkgs.rust-bin.stable."1.85.0".minimal.override { extensions = rsComponents; };
};
diff --git a/contrib/nix/smoke_test.sh b/contrib/nix/smoke_test.sh
new file mode 100755
index 00000000..74fb8163
--- /dev/null
+++ b/contrib/nix/smoke_test.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+set -uo pipefail
+
+out="${1:-$(mktemp -d)}"
+mkdir -p "${out}"
+printf '#include \nint main(void) { puts("Hello, world!"); return 0; }\n' > "${out}/hello.c"
+printf '#include \nint main() { std::cout << "Hello, world!\\n"; }\n' > "${out}/hello.cpp"
+
+status=0
+
+build() {
+ local name="$1" driver="$2" ext="$3" bin="${out}/${1}.${3}" kind
+ if [[ -z "${driver}" ]]; then
+ printf ' %-26s %-3s skipped\n' "${name}" "${ext}"
+ elif "${driver}" "${out}/hello.${ext}" -o "${bin}" && compgen -G "${bin}*" > /dev/null; then
+ kind=$(file -b "${bin}"* 2> /dev/null | head -1)
+ printf ' %-26s %-3s %s\n' "${name}" "${ext}" "${kind:-built}"
+ else
+ printf ' %-26s %-3s FAILED\n' "${name}" "${ext}"
+ status=1
+ fi
+}
+
+build host "${CC:-cc}" c
+build host "${CXX:-c++}" cpp
+
+for target in \
+ aarch64-unknown-linux-gnu \
+ x86_64-unknown-linux-gnu;
+do
+ cc="CC_${target//-/_}"
+ cxx="CXX_${target//-/_}"
+ build "${target}" "${!cc:-}" c
+ build "${target}" "${!cxx:-}" cpp
+done
+
+exit "${status}"
diff --git a/docs/dev/cross_compilation.md b/docs/dev/cross_compilation.md
new file mode 100644
index 00000000..50ce0d3e
--- /dev/null
+++ b/docs/dev/cross_compilation.md
@@ -0,0 +1,49 @@
+# Cross Compilation
+
+> [!NOTE]
+> This guide assumes access to a [development shell](./devshells.md). Attempting cross-compilation outside a devshell
+> is not within the scope of this guide and is unsupported.
+
+Cross-compilation is when the `host` and `target` of a binary diverge. Regular compilation has the goal of generating
+immediately executable artifacts for the machine that is compiling (i.e. they share identical `host` and `target`),
+while cross-compilation gains its value in being able to produce artifacts for consumption on other platforms without
+procuring hardware or platform configurations for each desired target.
+
+To achieve cross-compilation, the following are supplied:
+
+* A thinned-down root filesystem (i.e. sysroot) with headers and libraries expected by `target`
+* A compiler and linker that runs on the `host` but emits artifacts for the `target`
+* The standard library required by `target`
+
+We use an LLVM 20 toolchain (Clang as the compiler, LLD as the linker) for Rust packages that bind C/C++ codebases
+through an FFI, with the standard library bundle supplied to `rustc` by cargo. `rustc` itself is first class native
+cross-compiler.
+
+The following platforms are supported as `target`s **excluding the `host` platform**.
+
+| Target | Object Format | Sysroot |
+| --------------------------- | ------------- | --------------------------- |
+| `aarch64-unknown-linux-gnu` | ELF | glibc |
+| `x86_64-unknown-linux-gnu` | ELF | glibc |
+| `wasm32-unknown-unknown` | Wasm | *None*, no libc(++) support |
+
+For each target, the following environment variables are defined
+
+| Environment Variable | Description |
+| ------------------------------ | --------------------------------------- |
+| `CC_` | The C compiler for `target` |
+| `CXX_` | The C++ compiler for `target` |
+| `AR_` | The archiver, `llvm-ar` |
+| `CARGO_TARGET__LINKER` | The linker for `target` used by `cargo` |
+
+## Building (Rust)
+
+> [!TIP]
+> `--all-targets` is recommended when performing smoke tests to ensure the linker behaves as expected, since it
+> brings test binaries into scope. `base-sdk` is primarily a collection of library crates, so without binaries to
+> link, configuration failures may not surface.
+
+```bash
+# Building for ARM64 Linux (assuming an AMD64 host)
+cargo build --workspace --all-targets --features full --target aarch64-unknown-linux-gnu
+```
diff --git a/docs/zensical.toml b/docs/zensical.toml
index c797cfde..907fc245 100644
--- a/docs/zensical.toml
+++ b/docs/zensical.toml
@@ -18,6 +18,7 @@ nav = [
{ "Home" = "README.md" },
{ "Contributing" = [
{ "Getting Started" = "dev/getting_started.md" },
+ { "Cross Compilation" = "dev/cross_compilation.md" },
{ "Development Shells" = "dev/devshells.md" },
{ "Documentation" = "dev/about_docs.md" },
{ "Maintenance" = "dev/maintenance.md" },
From 54418d32948c1ead1cdb3b6b002d39b107a42fc7 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 07:17:12 +0530
Subject: [PATCH 13/17] sdk%feat(nix): introduce `#dev` shell for interactive
use
---
contrib/docker/entrypoint | 4 +--
contrib/nix/README.md | 4 +--
contrib/nix/flake.nix | 4 ++-
contrib/nix/shell/dev.nix | 68 +++++++++++++++++++++++++++++++++++++++
4 files changed, 75 insertions(+), 5 deletions(-)
create mode 100644 contrib/nix/shell/dev.nix
diff --git a/contrib/docker/entrypoint b/contrib/docker/entrypoint
index c04558a4..bba2dbcd 100755
--- a/contrib/docker/entrypoint
+++ b/contrib/docker/entrypoint
@@ -26,7 +26,7 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then
fi
if (( $# > 0 )); then
- exec nix develop ./contrib/nix#ci --command "$@"
+ exec nix develop ./contrib/nix#dev --command "$@"
else
- exec nix develop ./contrib/nix#ci
+ exec nix develop ./contrib/nix#dev
fi
diff --git a/contrib/nix/README.md b/contrib/nix/README.md
index bc057677..e7ee6f24 100644
--- a/contrib/nix/README.md
+++ b/contrib/nix/README.md
@@ -34,7 +34,7 @@ features need to be enabled (guidance for enablement should be taken from your d
To enter an interactive shell, from the repository root, use
```bash
-nix develop ./contrib/nix#ci
+nix develop ./contrib/nix#dev
```
### One-shot commands
@@ -42,7 +42,7 @@ nix develop ./contrib/nix#ci
To execute a command _without_ switching to a shell; or for scripting, use
```bash
-nix develop ./contrib/nix#ci --command cargo test --workspace --features full
+nix develop ./contrib/nix#dev --command cargo test --workspace --features full
```
diff --git a/contrib/nix/flake.nix b/contrib/nix/flake.nix
index 16a4204e..646aba9f 100644
--- a/contrib/nix/flake.nix
+++ b/contrib/nix/flake.nix
@@ -71,9 +71,11 @@
inherit pkgs lib inputs;
root = ../..;
};
+ ci = import ./shell/ci.nix ctx;
in
{
- ci = import ./shell/ci.nix ctx;
+ inherit ci;
+ dev = import ./shell/dev.nix (ctx // { inherit ci; });
}
);
diff --git a/contrib/nix/shell/dev.nix b/contrib/nix/shell/dev.nix
new file mode 100644
index 00000000..533c79ed
--- /dev/null
+++ b/contrib/nix/shell/dev.nix
@@ -0,0 +1,68 @@
+# Development shell for interactive instances
+
+{
+ ci,
+ lib,
+ pkgs,
+ ...
+}:
+
+let
+ ohMyBash = pkgs.fetchFromGitHub {
+ owner = "ohmybash";
+ repo = "oh-my-bash";
+ rev = "abf846186ab0a8a41ec5888e827ece6277dfe446";
+ hash = "sha256-tlYKhz7baZ02FcHlYMnLQsgjEsN/K+z3+UWre4mS5Qs=";
+ };
+
+ tmuxConf = pkgs.writeText "tmux.conf" ''
+ set -g default-terminal "tmux-256color"
+ set -ga terminal-overrides ",xterm-256color:Tc"
+ set -g alternate-screen off
+ set -g base-index 1
+ set -g detach-on-destroy on
+ set -g history-limit 20000
+ set -g mouse on
+ set -g pane-base-index 1
+ set -g renumber-windows on
+ '';
+
+ # tmux reads a file named on its command line or one under $HOME, and a
+ # devshell owns neither, so the flag is bound to the binary instead.
+ tmux = pkgs.writeShellScriptBin "tmux" ''
+ exec ${pkgs.tmux}/bin/tmux -f ${tmuxConf} "$@"
+ '';
+
+ utilities = [
+ pkgs.bashInteractive
+ pkgs.curl
+ pkgs.dnsutils
+ pkgs.fd
+ pkgs.gh
+ pkgs.gnupg
+ pkgs.jq
+ pkgs.nano
+ pkgs.pv
+ pkgs.ripgrep
+ pkgs.rsync
+ pkgs.unzip
+ pkgs.wget
+ tmux
+ ]
+ # Darwin carries `ping` in its base system, Linux does not.
+ ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.iputils ];
+in
+
+ci.overrideAttrs (old: {
+ # mkShell places `packages` here.
+ nativeBuildInputs = old.nativeBuildInputs ++ utilities;
+
+ # oh-my-bash is meant for interactive use. `--command` has no use for it.
+ shellHook = (old.shellHook or "") + ''
+ export OSH="${ohMyBash}"
+ if [[ $- == *i* ]]; then
+ OSH_THEME="rr"
+ source "$OSH/oh-my-bash.sh"
+ fi
+ '';
+})
From 8a24eeb395e386ac68de3132d4a811e7882d5025 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 03:08:30 +0530
Subject: [PATCH 14/17] sdk%feat(nix): cross-compile to Windows with MinGW-w64
on `#dev`
---
.github/workflows/build_cross.yml | 7 ++--
contrib/nix/mods/cxx.nix | 64 +++++++++++++++++++++++++++++--
contrib/nix/shell/common.nix | 5 +++
contrib/nix/shell/dev.nix | 37 ++++++++++++------
contrib/nix/smoke_test.sh | 3 +-
docs/dev/cross_compilation.md | 5 +++
6 files changed, 102 insertions(+), 19 deletions(-)
diff --git a/.github/workflows/build_cross.yml b/.github/workflows/build_cross.yml
index e5932964..2f536082 100644
--- a/.github/workflows/build_cross.yml
+++ b/.github/workflows/build_cross.yml
@@ -38,10 +38,10 @@ jobs:
skipAddingSubstituter: true
- name: Initialize devshell
- run: nix develop ./contrib/nix#ci --command true
+ run: nix develop ./contrib/nix#dev --command true
- name: Validate toolchains
- run: nix develop ./contrib/nix#ci --command ./contrib/nix/smoke_test.sh
+ run: nix develop ./contrib/nix#dev --command ./contrib/nix/smoke_test.sh
build:
name: ${{ matrix.target }}
@@ -52,10 +52,11 @@ jobs:
matrix:
target:
- x86_64-unknown-linux-gnu
+ - x86_64-pc-windows-gnu
defaults:
run:
- shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0}
+ shell: nix develop ./contrib/nix#dev --command bash -eo pipefail {0}
steps:
- name: Checkout
diff --git a/contrib/nix/mods/cxx.nix b/contrib/nix/mods/cxx.nix
index 6300d5d9..0b9abadb 100644
--- a/contrib/nix/mods/cxx.nix
+++ b/contrib/nix/mods/cxx.nix
@@ -25,8 +25,33 @@ let
kind = "glibc";
cross = pkgs.pkgsCross.gnu64;
};
+ "x86_64-pc-windows-gnu" = {
+ clangTarget = "x86_64-w64-mingw32";
+ kind = "mingw";
+ cross = pkgs.pkgsCross.mingwW64;
+ };
};
+ # MinGW keeps its headers and import libs apart, and libgcc comes from the
+ # cross GCC's libraries without its driver ever being invoked. clang emits
+ # crt2.o as a bare name, so -B is needed as well as -L.
+ mingwFlags =
+ d:
+ let
+ gccLib = "${d.cross.stdenv.cc.cc}/lib/gcc/x86_64-w64-mingw32/${d.cross.stdenv.cc.cc.version}";
+ in
+ [
+ "-isystem ${d.cross.windows.mingw_w64_headers}/include"
+ "-B${d.cross.windows.mingw_w64}/lib"
+ "-B${gccLib}"
+ "-L${d.cross.windows.mingw_w64}/lib"
+ "-L${gccLib}"
+ # rustc's windows-gnu spec links -l:libpthread.a by that literal name,
+ # and rust-std ships no self-contained copy of it.
+ "-L${d.cross.windows.pthreads}/lib"
+ (ldFor "ld.lld")
+ ];
+
# glibc splits its outputs, so there isn't a unified tree to hand --sysroot
# headers are in .dev, crt objects and libraries are in .out. libgcc_s.so
# is a third output independent of the compiler's libraries.
@@ -58,8 +83,40 @@ let
"-L${cc}/${d.clangTarget}/lib"
];
- cxxExtra = d: { glibc = libStdCxx; }.${d.kind} d;
- flagsFor = d: { glibc = glibcFlags; }.${d.kind} d;
+ # libstdc++ here was built with the mcf threading model, not the winpthreads
+ # rustc asks for, so it needs mcfgthread's headers and _MCF_* symbols. gcc
+ # names the library through its spec file; clang has none, so it is here.
+ mingwLibStdCxx =
+ d:
+ libStdCxx d
+ ++ [
+ "-isystem ${d.cross.windows.mcfgthreads.dev}/include"
+ "-L${d.cross.windows.mcfgthreads}/lib"
+ "-lmcfgthread"
+ ];
+
+ cxxExtra =
+ d:
+ {
+ glibc = libStdCxx;
+ mingw = mingwLibStdCxx;
+ }
+ .${d.kind}
+ d;
+
+ flagsFor =
+ d:
+ {
+ mingw = mingwFlags;
+ glibc = glibcFlags;
+ }
+ .${d.kind}
+ d;
+
+ # rustc shells out to -dlltool for the raw-dylib imports windows-sys
+ # declares, and looks for that exact name, not llvm-dlltool. Every binary in
+ # this package is target-prefixed, so none of it shadows a host tool.
+ extraPkgs = d: lib.optionals (d.kind == "mingw") [ d.cross.stdenv.cc.bintools.bintools ];
# A driver per target and language. C_INCLUDE_PATH and CPLUS_INCLUDE_PATH
# are unset because the host's include paths would otherwise leak into a
@@ -90,7 +147,8 @@ let
packages = [
cc
cxx
- ];
+ ]
+ ++ extraPkgs d;
env = {
"CC_${ccKey t}" = "${cc}/bin/${t}-cc";
"CXX_${ccKey t}" = "${cxx}/bin/${t}-c++";
diff --git a/contrib/nix/shell/common.nix b/contrib/nix/shell/common.nix
index 56ad202d..e13be475 100644
--- a/contrib/nix/shell/common.nix
+++ b/contrib/nix/shell/common.nix
@@ -19,6 +19,10 @@ let
sameOs = t: lib.hasInfix (if pkgs.stdenv.hostPlatform.isDarwin then "apple-darwin" else "linux") t;
crossTargets = lib.filter (t: t != hostTriple && sameOs t) cxx.knownTargets;
+ # Everything else the table knows, which needs a sysroot of its own and so
+ # only the dev shell carries.
+ foreignTargets = lib.filter (t: t != hostTriple && !sameOs t) cxx.knownTargets;
+
nightlyWith =
extra:
(pkgs.rust-bin.fromRustupToolchainFile (root + "/rust-toolchain.toml")).override {
@@ -61,6 +65,7 @@ in
compose
crossTargets
cxx
+ foreignTargets
nightlyWith
;
diff --git a/contrib/nix/shell/dev.nix b/contrib/nix/shell/dev.nix
index 533c79ed..2a6d3ae9 100644
--- a/contrib/nix/shell/dev.nix
+++ b/contrib/nix/shell/dev.nix
@@ -2,12 +2,19 @@
{
ci,
+ cxx,
+ crossTargets,
+ foreignTargets,
lib,
+ nightlyWith,
pkgs,
...
}:
let
+ cross = cxx.forTargets foreignTargets;
+ toolchain = nightlyWith (crossTargets ++ foreignTargets);
+
ohMyBash = pkgs.fetchFromGitHub {
owner = "ohmybash";
repo = "oh-my-bash";
@@ -53,16 +60,22 @@ let
++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.iputils ];
in
-ci.overrideAttrs (old: {
- # mkShell places `packages` here.
- nativeBuildInputs = old.nativeBuildInputs ++ utilities;
+ci.overrideAttrs (
+ old:
+ {
+ # mkShell places `packages` here.
+ nativeBuildInputs = old.nativeBuildInputs ++ cross.packages ++ utilities ++ [ toolchain ];
- # oh-my-bash is meant for interactive use. `--command` has no use for it.
- shellHook = (old.shellHook or "") + ''
- export OSH="${ohMyBash}"
- if [[ $- == *i* ]]; then
- OSH_THEME="rr"
- source "$OSH/oh-my-bash.sh"
- fi
- '';
-})
+ # oh-my-bash is meant for interactive use. `--command` has no use for it.
+ shellHook = (old.shellHook or "") + ''
+ export PATH="${toolchain}/bin:$PATH"
+
+ export OSH="${ohMyBash}"
+ if [[ $- == *i* ]]; then
+ OSH_THEME="rr"
+ source "$OSH/oh-my-bash.sh"
+ fi
+ '';
+ }
+ // cross.env
+)
diff --git a/contrib/nix/smoke_test.sh b/contrib/nix/smoke_test.sh
index 74fb8163..38a7230e 100755
--- a/contrib/nix/smoke_test.sh
+++ b/contrib/nix/smoke_test.sh
@@ -27,7 +27,8 @@ build host "${CXX:-c++}" cpp
for target in \
aarch64-unknown-linux-gnu \
- x86_64-unknown-linux-gnu;
+ x86_64-unknown-linux-gnu \
+ x86_64-pc-windows-gnu;
do
cc="CC_${target//-/_}"
cxx="CXX_${target//-/_}"
diff --git a/docs/dev/cross_compilation.md b/docs/dev/cross_compilation.md
index 50ce0d3e..b7798b26 100644
--- a/docs/dev/cross_compilation.md
+++ b/docs/dev/cross_compilation.md
@@ -19,12 +19,17 @@ We use an LLVM 20 toolchain (Clang as the compiler, LLD as the linker) for Rust
through an FFI, with the standard library bundle supplied to `rustc` by cargo. `rustc` itself is first class native
cross-compiler.
+> [!NOTE]
+> Support for Windows cross-compilation is only available in the `#dev` devshell, it is omitted from the `#ci` devshell
+> to reduce cache contention with our forge provider.
+
The following platforms are supported as `target`s **excluding the `host` platform**.
| Target | Object Format | Sysroot |
| --------------------------- | ------------- | --------------------------- |
| `aarch64-unknown-linux-gnu` | ELF | glibc |
| `x86_64-unknown-linux-gnu` | ELF | glibc |
+| `x86_64-pc-windows-gnu` | PE32+ | MinGW-w64 |
| `wasm32-unknown-unknown` | Wasm | *None*, no libc(++) support |
For each target, the following environment variables are defined
From 9c1d6fa561b447cc07921ded420307dedcae9144 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 03:07:12 +0530
Subject: [PATCH 15/17] sdk%feat(nix): cross-compile to macOS with
Xcode-sourced SDK on `#dev`
---
.github/workflows/build_cross.yml | 1 +
contrib/nix/flake.nix | 1 +
contrib/nix/mods/cxx.nix | 50 +++++++++++++++++++++++++++++--
contrib/nix/mods/rust.nix | 16 ++--------
contrib/nix/mods/xcode_sdk.nix | 42 ++++++++++++++++++++++++++
contrib/nix/shell/common.nix | 28 +++++++++++++----
contrib/nix/smoke_test.sh | 2 ++
docs/dev/cross_compilation.md | 10 +++++--
8 files changed, 125 insertions(+), 25 deletions(-)
create mode 100644 contrib/nix/mods/xcode_sdk.nix
diff --git a/.github/workflows/build_cross.yml b/.github/workflows/build_cross.yml
index 2f536082..13a53278 100644
--- a/.github/workflows/build_cross.yml
+++ b/.github/workflows/build_cross.yml
@@ -53,6 +53,7 @@ jobs:
target:
- x86_64-unknown-linux-gnu
- x86_64-pc-windows-gnu
+ - aarch64-apple-darwin
defaults:
run:
diff --git a/contrib/nix/flake.nix b/contrib/nix/flake.nix
index 646aba9f..ff9e5430 100644
--- a/contrib/nix/flake.nix
+++ b/contrib/nix/flake.nix
@@ -57,6 +57,7 @@
pkg:
builtins.elem (lib.getName pkg) [
"codeql"
+ "xcode-sdk"
];
overlays = [ rust-overlay.overlays.default ];
}
diff --git a/contrib/nix/mods/cxx.nix b/contrib/nix/mods/cxx.nix
index 0b9abadb..0ccf8563 100644
--- a/contrib/nix/mods/cxx.nix
+++ b/contrib/nix/mods/cxx.nix
@@ -1,6 +1,10 @@
# LLVM C(++) compiler setup and configuration
-{ pkgs, lib }:
+{
+ pkgs,
+ lib,
+ xcodeSdk ? null,
+}:
let
llvm = pkgs.llvmPackages_20;
@@ -15,6 +19,14 @@ let
# Per-target definitions keyed against `hostTriple`
defs = {
+ "aarch64-apple-darwin" = {
+ clangTarget = "arm64-apple-darwin";
+ kind = "darwin";
+ };
+ "x86_64-apple-darwin" = {
+ clangTarget = "x86_64-apple-darwin";
+ kind = "darwin";
+ };
"aarch64-unknown-linux-gnu" = {
clangTarget = "aarch64-unknown-linux-gnu";
kind = "glibc";
@@ -70,6 +82,21 @@ let
(ldFor "ld.lld")
];
+ darwinFlags =
+ _:
+ [
+ "-isysroot ${xcodeSdk}"
+ "-nostdlibinc"
+ "-iwithsysroot/usr/include"
+ "-iframeworkwithsysroot/System/Library/Frameworks"
+ "-mmacos-version-min=${xcodeSdk.minVersion}"
+ (ldFor "ld64.lld")
+ ]
+ ++ lib.optionals (!pkgs.stdenv.hostPlatform.isDarwin) [
+ "-mlinker-version=${xcodeSdk.linkerVersion}"
+ "-Wl,-no_adhoc_codesign"
+ ];
+
# libstdc++ comes from the cross GCC rather than from the sysroot
libStdCxx =
d:
@@ -95,9 +122,15 @@ let
"-lmcfgthread"
];
+ # libc++ is part of the SDK, which carries its headers and its link stub.
+ # These precede the C headers, since libc++ resolves its own
+ # first and errors out if it cannot.
+ libCxx = _: [ "-iwithsysroot/usr/include/c++/v1" ];
+
cxxExtra =
d:
{
+ darwin = libCxx;
glibc = libStdCxx;
mingw = mingwLibStdCxx;
}
@@ -109,6 +142,7 @@ let
{
mingw = mingwFlags;
glibc = glibcFlags;
+ darwin = darwinFlags;
}
.${d.kind}
d;
@@ -136,10 +170,12 @@ let
ccKey = t: builtins.replaceStrings [ "-" ] [ "_" ] t;
cargoKey = t: lib.toUpper (ccKey t);
+ defFor = t: defs.${t} or (throw "cxx.nix knows no C toolchain for ${t}");
+
wire =
t:
let
- d = defs.${t} or (throw "cxx.nix knows no C toolchain for ${t}");
+ d = defFor t;
cc = driver "${t}-cc" "clang" d [ ];
cxx = driver "${t}-c++" "clang++" d (cxxExtra d);
in
@@ -173,9 +209,17 @@ in
targets:
let
wired = map wire targets;
+
+ # `rustc` asks `xcrun` for the SDK and picks its own deployment target, so
+ # both are set shell-wide rather than per target. A native macOS build
+ # has to resolve against the same pinned SDK as a cross target.
+ darwin = lib.optionalAttrs (lib.any (t: (defFor t).kind == "darwin") targets) {
+ MACOSX_DEPLOYMENT_TARGET = xcodeSdk.minVersion;
+ SDKROOT = "${xcodeSdk}";
+ };
in
{
packages = lib.concatMap (w: w.packages) wired;
- env = lib.foldl' (a: w: a // w.env) { } wired;
+ env = lib.foldl' (a: w: a // w.env) darwin wired;
};
}
diff --git a/contrib/nix/mods/rust.nix b/contrib/nix/mods/rust.nix
index 66ae95a9..32593b25 100644
--- a/contrib/nix/mods/rust.nix
+++ b/contrib/nix/mods/rust.nix
@@ -3,29 +3,17 @@
{
default,
lib,
- pkgs,
toolchains,
}:
let
- # Purge C compiler wrapper propagated by rust-overlay to prioritize
- # stdenv's C compiler (defined in cxx.nix)
- bare = lib.mapAttrs (
- _: t:
- t.overrideAttrs (_: {
- propagatedBuildInputs = [ ];
- depsHostHostPropagated = [ ];
- depsTargetTargetPropagated = [ ];
- })
- ) toolchains;
-
# A path per non-default toolchain, since only one can own `cargo` at a time.
named = lib.mapAttrs' (name: t: lib.nameValuePair "TOOLCHAIN_${lib.toUpper name}" "${t}") (
- lib.filterAttrs (name: _: name != default) bare
+ lib.filterAttrs (name: _: name != default) toolchains
);
in
{
- packages = [ bare.${default} ];
+ packages = [ toolchains.${default} ];
env = {
CARGO_TERM_COLOR = "always";
diff --git a/contrib/nix/mods/xcode_sdk.nix b/contrib/nix/mods/xcode_sdk.nix
new file mode 100644
index 00000000..695e6427
--- /dev/null
+++ b/contrib/nix/mods/xcode_sdk.nix
@@ -0,0 +1,42 @@
+# Xcode-derived macOS SDK with libcxx headers
+
+{ pkgs }:
+
+let
+ # Xcode release and build ID
+ version = "26.1.1-17B100";
+in
+
+pkgs.stdenvNoCC.mkDerivation {
+ inherit version;
+
+ # Archive contains stubs and headers, nothing to configure, build or fix
+ dontBuild = true;
+ dontConfigure = true;
+ dontFixup = true;
+
+ pname = "xcode-sdk";
+ src = pkgs.fetchurl {
+ url = "https://bitcoincore.org/depends-sources/sdks/Xcode-${version}-extracted-SDK-with-libcxx-headers.tar";
+ hash = "sha256-lgD6k2RN9nTukWteLIprqNrPYxmWpl3JItADuYteo7E=";
+ };
+
+ installPhase = ''
+ runHook preInstall
+ mkdir -p $out
+ cp -a ./* $out/
+ runHook postInstall
+ '';
+
+ passthru = {
+ # Target macOS version binaries are expected to support.
+ minVersion = "14.0";
+ # ld64 version clang is told to assume, as lld reports its own.
+ linkerVersion = "711";
+ };
+
+ meta = {
+ description = "macOS SDK extracted from Xcode, with libc++ headers";
+ license = pkgs.lib.licenses.unfree;
+ };
+}
diff --git a/contrib/nix/shell/common.nix b/contrib/nix/shell/common.nix
index e13be475..434b8f7b 100644
--- a/contrib/nix/shell/common.nix
+++ b/contrib/nix/shell/common.nix
@@ -11,7 +11,10 @@ let
# Target platform for web demos bundled with documentation.
commonTargets = [ "wasm32-unknown-unknown" ];
- cxx = import ../mods/cxx.nix { inherit pkgs lib; };
+ cxx = import ../mods/cxx.nix {
+ inherit pkgs lib;
+ xcodeSdk = import ../mods/xcode_sdk.nix { inherit pkgs; };
+ };
rsComponents = (lib.importTOML (root + "/rust-toolchain.toml")).toolchain.components;
@@ -23,11 +26,24 @@ let
# only the dev shell carries.
foreignTargets = lib.filter (t: t != hostTriple && !sameOs t) cxx.knownTargets;
+ # rust-overlay propagates a C compiler wrapper with every toolchain, which
+ # would take precedence over `cxx.nix`'s definitions. We strip it here so that
+ # every shell reaching for a toolchain respects our definitions.
+ bare =
+ toolchain:
+ toolchain.overrideAttrs (_: {
+ propagatedBuildInputs = [ ];
+ depsHostHostPropagated = [ ];
+ depsTargetTargetPropagated = [ ];
+ });
+
nightlyWith =
extra:
- (pkgs.rust-bin.fromRustupToolchainFile (root + "/rust-toolchain.toml")).override {
- targets = commonTargets ++ extra;
- };
+ bare (
+ (pkgs.rust-bin.fromRustupToolchainFile (root + "/rust-toolchain.toml")).override {
+ targets = commonTargets ++ extra;
+ }
+ );
# Folds modules into mkShell arguments. Conflicting variables or stdenvs
# will throw instead of allowing order-sensitive assignment.
@@ -81,12 +97,12 @@ in
python = pkgs.python311;
};
rust = import ../mods/rust.nix {
- inherit pkgs lib;
+ inherit lib;
default = "nightly";
toolchains = {
nightly = nightlyWith crossTargets;
# Must match `workspace.package.rust-version` in root Cargo.toml.
- msrv = pkgs.rust-bin.stable."1.85.0".minimal.override { extensions = rsComponents; };
+ msrv = bare (pkgs.rust-bin.stable."1.85.0".minimal.override { extensions = rsComponents; });
};
};
};
diff --git a/contrib/nix/smoke_test.sh b/contrib/nix/smoke_test.sh
index 38a7230e..6f46cb0f 100755
--- a/contrib/nix/smoke_test.sh
+++ b/contrib/nix/smoke_test.sh
@@ -26,6 +26,8 @@ build host "${CC:-cc}" c
build host "${CXX:-c++}" cpp
for target in \
+ aarch64-apple-darwin \
+ x86_64-apple-darwin \
aarch64-unknown-linux-gnu \
x86_64-unknown-linux-gnu \
x86_64-pc-windows-gnu;
diff --git a/docs/dev/cross_compilation.md b/docs/dev/cross_compilation.md
index b7798b26..84ed10c9 100644
--- a/docs/dev/cross_compilation.md
+++ b/docs/dev/cross_compilation.md
@@ -20,18 +20,24 @@ through an FFI, with the standard library bundle supplied to `rustc` by cargo. `
cross-compiler.
> [!NOTE]
-> Support for Windows cross-compilation is only available in the `#dev` devshell, it is omitted from the `#ci` devshell
-> to reduce cache contention with our forge provider.
+> Support for macOS and Windows cross-compilation is only available in the `#dev` devshell, it is omitted from the `#ci`
+> devshell to reduce cache contention with our forge provider.
The following platforms are supported as `target`s **excluding the `host` platform**.
| Target | Object Format | Sysroot |
| --------------------------- | ------------- | --------------------------- |
+| `aarch64-apple-darwin` | Mach-O | macOS SDK with libcxx |
+| `x86_64-apple-darwin` | Mach-O | macOS SDK with libcxx |
| `aarch64-unknown-linux-gnu` | ELF | glibc |
| `x86_64-unknown-linux-gnu` | ELF | glibc |
| `x86_64-pc-windows-gnu` | PE32+ | MinGW-w64 |
| `wasm32-unknown-unknown` | Wasm | *None*, no libc(++) support |
+> [!NOTE]
+> `` spells the triple with underscores, as `cc-rs` reads it (e.g. `CC_aarch64_apple_darwin`), while
+> `` spells the triple in upper case, as `cargo` reads it (e.g. `CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER`).
+
For each target, the following environment variables are defined
| Environment Variable | Description |
From 75a7404e0eb3645cc6c58a9ec0e075a8c1c6297e Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 03:22:04 +0530
Subject: [PATCH 16/17] sdk%test(nix): smoke test Rust cross-compilation
---
contrib/nix/smoke_test.sh | 43 ++++++++++++++++++++++++++-------------
1 file changed, 29 insertions(+), 14 deletions(-)
diff --git a/contrib/nix/smoke_test.sh b/contrib/nix/smoke_test.sh
index 6f46cb0f..52fe2a27 100755
--- a/contrib/nix/smoke_test.sh
+++ b/contrib/nix/smoke_test.sh
@@ -6,36 +6,51 @@ out="${1:-$(mktemp -d)}"
mkdir -p "${out}"
printf '#include \nint main(void) { puts("Hello, world!"); return 0; }\n' > "${out}/hello.c"
printf '#include \nint main() { std::cout << "Hello, world!\\n"; }\n' > "${out}/hello.cpp"
+printf 'fn main() { println!("Hello, world!"); }\n' > "${out}/hello.rs"
status=0
-build() {
- local name="$1" driver="$2" ext="$3" bin="${out}/${1}.${3}" kind
- if [[ -z "${driver}" ]]; then
- printf ' %-26s %-3s skipped\n' "${name}" "${ext}"
- elif "${driver}" "${out}/hello.${ext}" -o "${bin}" && compgen -G "${bin}*" > /dev/null; then
- kind=$(file -b "${bin}"* 2> /dev/null | head -1)
- printf ' %-26s %-3s %s\n' "${name}" "${ext}" "${kind:-built}"
- else
- printf ' %-26s %-3s FAILED\n' "${name}" "${ext}"
+say() {
+ printf ' %-26s %-4s %s\n' "$1" "$2" "$3"
+}
+
+try() {
+ local name="$1" lang="$2" bin="${out}/${1}.${2}" kind
+ shift 2
+ if [[ -z "$1" ]]; then
+ say "${name}" "${lang}" skipped
+ return
+ fi
+ "$@" "${out}/hello.${lang}" -o "${bin}"
+ if ! compgen -G "${bin}*" > /dev/null; then
+ say "${name}" "${lang}" FAILED
status=1
+ else
+ kind=$(file -b "${bin}"* 2> /dev/null | head -1)
+ say "${name}" "${lang}" "${kind:-built}"
fi
}
-build host "${CC:-cc}" c
-build host "${CXX:-c++}" cpp
+try host c "${CC:-cc}"
+try host cpp "${CXX:-c++}"
+try host rs rustc
for target in \
aarch64-apple-darwin \
x86_64-apple-darwin \
aarch64-unknown-linux-gnu \
x86_64-unknown-linux-gnu \
- x86_64-pc-windows-gnu;
+ x86_64-pc-windows-gnu \
+ wasm32-unknown-unknown;
do
cc="CC_${target//-/_}"
cxx="CXX_${target//-/_}"
- build "${target}" "${!cc:-}" c
- build "${target}" "${!cxx:-}" cpp
+ driver="${!cc:-}"
+ rust="${driver:+rustc}"
+ [[ "${target}" != wasm32-* ]] || rust=rustc
+ try "${target}" c "${driver}"
+ try "${target}" cpp "${!cxx:-}"
+ try "${target}" rs "${rust}" "--target=${target}" ${driver:+"-Clinker=${driver}"}
done
exit "${status}"
From 4ff23401c7fd76fbfb6b925c3b541bdc490138a5 Mon Sep 17 00:00:00 2001
From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com>
Date: Tue, 8 Sep 2026 03:22:28 +0530
Subject: [PATCH 17/17] sdk%test(nix): validate macOS-specific invariants
---
contrib/nix/smoke_test.sh | 30 ++++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
diff --git a/contrib/nix/smoke_test.sh b/contrib/nix/smoke_test.sh
index 52fe2a27..6c3ec9f5 100755
--- a/contrib/nix/smoke_test.sh
+++ b/contrib/nix/smoke_test.sh
@@ -10,25 +10,43 @@ printf 'fn main() { println!("Hello, world!"); }\n' > "${out}/hello.rs"
status=0
+# `--macho` is a `llvm-objdump` flag, the GNU equivalent is unsupported.
+objdump=$(command -v llvm-objdump || command -v objdump || true)
+
say() {
printf ' %-26s %-4s %s\n' "$1" "$2" "$3"
}
+produced() {
+ local found=("${1}"*)
+ [[ -e "${found[0]}" ]]
+}
+
try() {
- local name="$1" lang="$2" bin="${out}/${1}.${2}" kind
+ local name="$1" lang="$2" bin="${out}/${1}.${2}" want="${MACOSX_DEPLOYMENT_TARGET:-}" got kind
shift 2
if [[ -z "$1" ]]; then
say "${name}" "${lang}" skipped
return
fi
- "$@" "${out}/hello.${lang}" -o "${bin}"
- if ! compgen -G "${bin}*" > /dev/null; then
+ rm -f -- "${bin}"*
+ if ! "$@" "${out}/hello.${lang}" -o "${bin}" || ! produced "${bin}"; then
say "${name}" "${lang}" FAILED
status=1
- else
- kind=$(file -b "${bin}"* 2> /dev/null | head -1)
- say "${name}" "${lang}" "${kind:-built}"
+ return
+ fi
+ # An unreadable load command is a failure, not a reason to skip the check.
+ if [[ "${name}" == *-apple-darwin ]]; then
+ got=$("${objdump:-false}" --macho --private-headers "${bin}"* 2> /dev/null |
+ awk '$1 == "minos" { print $2; exit }')
+ if [[ -z "${want}" || "${got}" != "${want}" ]]; then
+ say "${name}" "${lang}" "FAILED, minos ${got:-unknown}, wanted ${want:-}"
+ status=1
+ return
+ fi
fi
+ kind=$(file -b "${bin}"* 2> /dev/null | head -1)
+ say "${name}" "${lang}" "${kind:-built}"
}
try host c "${CC:-cc}"