diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2f36ac..72f1009 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,6 +58,8 @@ jobs: include: - target: x86_64-unknown-linux-gnu os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-24.04-arm - target: x86_64-apple-darwin os: macos-14-large - target: aarch64-apple-darwin @@ -168,3 +170,49 @@ jobs: sha256sums.txt env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + npm: + name: Publish to npm + needs: [prepare, build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.tag }} + + - uses: actions/setup-node@v4 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + pattern: release-* + path: artifacts + + - name: Extract binaries by target + run: | + # artifacts/release-/aleo-devnode--.zip + # -> bins//aleo-devnode[.exe] + for dir in artifacts/release-*; do + target="${dir#artifacts/release-}" + mkdir -p "bins/$target" + unzip -o "$dir"/*.zip -d "bins/$target" + done + + - name: Build npm packages + run: node scripts/build-npm.mjs --version "${{ needs.prepare.outputs.version }}" --artifacts bins --out dist-npm + + - name: Publish + run: | + set -e + # Platform packages first, then the main launcher last, so main's + # exact-pinned optionalDependencies always resolve on install. + for pkg in dist-npm/*/; do + [ "$pkg" = "dist-npm/main/" ] && continue + npm publish "$pkg" --access public + done + npm publish dist-npm/main/ --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 0000000..37196e4 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,34 @@ +# aleo-devnode + +Prebuilt binary distribution of [`aleo-devnode`](https://github.com/ProvableHQ/aleo-devnode) — a local Aleo development node that skips proof verification so clients can broadcast transactions built with placeholder proofs. + +## Install + +```sh +npm install -g @provablehq/aleo-devnode +``` + +This installs a tiny launcher plus one platform-specific binary package (selected automatically by npm via `os`/`cpu`). Supported platforms: + +| OS | Arch | +| ------- | ----- | +| macOS | arm64 | +| macOS | x64 | +| Linux | x64 | +| Linux | arm64 | +| Windows | x64 | + +## Usage + +```sh +aleo-devnode accounts +aleo-devnode start --private-key +``` + +All arguments are forwarded directly to the underlying binary. See the [project README](https://github.com/ProvableHQ/aleo-devnode) for full CLI documentation. + +## How it works + +The `@provablehq/aleo-devnode` package declares each platform binary as an `optionalDependency`. npm installs only the one whose `os`/`cpu` match the host. The `aleo-devnode` command is a thin Node shim that resolves the matching package and execs the real binary. + +If you install with `--no-optional` (or `--omit=optional`), the binary won't be present; reinstall without that flag. diff --git a/npm/run.js b/npm/run.js new file mode 100644 index 0000000..0c0e9aa --- /dev/null +++ b/npm/run.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +"use strict"; + +// Thin launcher for the aleo-devnode binary. The actual platform-specific +// binary ships in an optional dependency (e.g. @provablehq/aleo-devnode-darwin-arm64); +// npm installs only the one matching the host's os/cpu. This shim resolves that +// package and execs the binary, forwarding argv, stdio, and the exit code. + +const { spawnSync } = require("node:child_process"); + +// suffix -> [process.platform, process.arch] +const PLATFORMS = { + "darwin-arm64": ["darwin", "arm64"], + "darwin-x64": ["darwin", "x64"], + "linux-x64": ["linux", "x64"], + "linux-arm64": ["linux", "arm64"], + "win32-x64": ["win32", "x64"], +}; + +function resolveBinary() { + const suffix = Object.keys(PLATFORMS).find( + (k) => PLATFORMS[k][0] === process.platform && PLATFORMS[k][1] === process.arch, + ); + if (!suffix) { + return { error: `unsupported platform: ${process.platform}-${process.arch}` }; + } + const pkg = `@provablehq/aleo-devnode-${suffix}`; + const binName = process.platform === "win32" ? "aleo-devnode.exe" : "aleo-devnode"; + try { + return { path: require.resolve(`${pkg}/bin/${binName}`) }; + } catch { + return { + error: + `the platform package "${pkg}" is not installed.\n` + + `It is normally installed automatically as an optional dependency; a\n` + + `"--no-optional" / "--omit=optional" install will skip it. Reinstall\n` + + `without that flag, or add the package directly:\n` + + ` npm install ${pkg}`, + }; + } +} + +const resolved = resolveBinary(); +if (resolved.error) { + console.error(`aleo-devnode: ${resolved.error}`); + process.exit(1); +} + +const result = spawnSync(resolved.path, process.argv.slice(2), { stdio: "inherit" }); +if (result.error) { + console.error(`aleo-devnode: failed to launch binary: ${result.error.message}`); + process.exit(1); +} +// Propagate the child's exit code; if it was killed by a signal, exit non-zero. +process.exit(typeof result.status === "number" ? result.status : 1); diff --git a/scripts/build-npm.mjs b/scripts/build-npm.mjs new file mode 100644 index 0000000..a52ea9d --- /dev/null +++ b/scripts/build-npm.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node +// Assembles the npm packages for release into /: +// - // one per platform, each carrying the compiled binary +// - /main/ the @provablehq/aleo-devnode launcher package +// +// Inputs are the release binaries the GitHub Actions build matrix produced, +// laid out as //aleo-devnode[.exe]. +// +// Usage: +// node scripts/build-npm.mjs --version 0.2.0 --artifacts bins --out dist-npm +// +// The publish step (release.yml) publishes every platform package first, then +// main last, so main's exact-pinned optionalDependencies always resolve. + +import { existsSync, mkdirSync, copyFileSync, writeFileSync, chmodSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const SCOPE = "@provablehq"; +const BASE = "aleo-devnode"; +const LICENSE = "Apache-2.0"; +const REPO = "https://github.com/ProvableHQ/aleo-devnode"; + +// Single source of truth for the target matrix. `target` must match the +// directory names produced by the release build matrix (the Rust triples). +const PLATFORMS = [ + { suffix: "darwin-arm64", target: "aarch64-apple-darwin", os: "darwin", cpu: "arm64" }, + { suffix: "darwin-x64", target: "x86_64-apple-darwin", os: "darwin", cpu: "x64" }, + { suffix: "linux-x64", target: "x86_64-unknown-linux-gnu", os: "linux", cpu: "x64" }, + { suffix: "linux-arm64", target: "aarch64-unknown-linux-gnu", os: "linux", cpu: "arm64" }, + { suffix: "win32-x64", target: "x86_64-pc-windows-msvc", os: "win32", cpu: "x64" }, +]; + +function parseArgs(argv) { + const args = {}; + for (let i = 0; i < argv.length; i += 2) { + const key = argv[i]; + if (!key?.startsWith("--")) throw new Error(`unexpected argument: ${key}`); + args[key.slice(2)] = argv[i + 1]; + } + return args; +} + +const { version, artifacts, out } = parseArgs(process.argv.slice(2)); +if (!version || !artifacts || !out) { + console.error("usage: build-npm.mjs --version --artifacts --out "); + process.exit(1); +} + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const shimSrc = resolve(scriptDir, "..", "npm", "run.js"); +const readmeSrc = resolve(scriptDir, "..", "npm", "README.md"); +const artifactsDir = resolve(artifacts); +const outDir = resolve(out); + +const writeJson = (path, obj) => writeFileSync(path, JSON.stringify(obj, null, 2) + "\n"); + +// --- platform packages ----------------------------------------------------- +for (const p of PLATFORMS) { + const binName = p.os === "win32" ? "aleo-devnode.exe" : "aleo-devnode"; + const binSrc = join(artifactsDir, p.target, binName); + if (!existsSync(binSrc)) { + throw new Error(`missing binary for ${p.target}: expected ${binSrc}`); + } + + const pkgDir = join(outDir, p.suffix); + const binDir = join(pkgDir, "bin"); + mkdirSync(binDir, { recursive: true }); + + copyFileSync(binSrc, join(binDir, binName)); + if (p.os !== "win32") chmodSync(join(binDir, binName), 0o755); + + writeJson(join(pkgDir, "package.json"), { + name: `${SCOPE}/${BASE}-${p.suffix}`, + version, + description: `Prebuilt aleo-devnode binary for ${p.suffix}`, + license: LICENSE, + repository: { type: "git", url: `git+${REPO}.git` }, + os: [p.os], + cpu: [p.cpu], + files: ["bin/"], + }); + console.log(`built ${SCOPE}/${BASE}-${p.suffix}`); +} + +// --- main launcher package ------------------------------------------------- +const mainDir = join(outDir, "main"); +mkdirSync(mainDir, { recursive: true }); +copyFileSync(shimSrc, join(mainDir, "run.js")); +copyFileSync(readmeSrc, join(mainDir, "README.md")); + +writeJson(join(mainDir, "package.json"), { + name: `${SCOPE}/${BASE}`, + version, + description: "Local Aleo development node (prebuilt binary distribution)", + license: LICENSE, + repository: { type: "git", url: `git+${REPO}.git` }, + bin: { "aleo-devnode": "run.js" }, + files: ["run.js", "README.md"], + engines: { node: ">=18" }, + optionalDependencies: Object.fromEntries( + PLATFORMS.map((p) => [`${SCOPE}/${BASE}-${p.suffix}`, version]), + ), +}); +console.log(`built ${SCOPE}/${BASE} (main)`); + +// Sanity check: the shim's platform table must match this script's matrix, +// otherwise a published platform package would never be resolved at runtime. +const shim = readFileSync(shimSrc, "utf8"); +for (const p of PLATFORMS) { + if (!shim.includes(`"${p.suffix}"`)) { + throw new Error(`npm/run.js is missing platform "${p.suffix}" — matrix drift`); + } +}