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}"