From 71387e94ae4b01294c4c27b83ccc2c6e6f1b8270 Mon Sep 17 00:00:00 2001 From: w00c00 <46839097+w00c00@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:02:04 +0800 Subject: [PATCH 1/4] Add native Windows and Linux releases --- .github/workflows/release-desktop.yml | 158 ++++++++++++++++++++ CHANGELOG.md | 14 ++ README.md | 12 +- docs/releases/v0.2.1.md | 51 +++++++ package-lock.json | 4 +- package.json | 6 +- scripts/build-kascov-preflight.mjs | 41 +++++ scripts/build-kascov-preflight.sh | 30 +--- scripts/build-silverc.mjs | 98 ++++++++++++ scripts/build-silverc.sh | 64 +------- scripts/check-syntax.mjs | 2 +- scripts/platform-binaries.mjs | 22 +++ scripts/prepare-desktop-runtime.mjs | 26 ++-- scripts/write-kascov-preflight-manifest.mjs | 7 +- scripts/write-release-checksums.mjs | 19 +++ server/compiler.mjs | 70 ++++++--- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/build.rs | 2 +- src-tauri/src/lib.rs | 101 +++++++------ src-tauri/src/main.rs | 2 +- src-tauri/tauri.conf.json | 2 +- test/studio.test.mjs | 21 +++ 23 files changed, 571 insertions(+), 185 deletions(-) create mode 100644 .github/workflows/release-desktop.yml create mode 100644 docs/releases/v0.2.1.md create mode 100644 scripts/build-kascov-preflight.mjs create mode 100644 scripts/build-silverc.mjs create mode 100644 scripts/platform-binaries.mjs create mode 100644 scripts/write-release-checksums.mjs diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml new file mode 100644 index 0000000..331c62b --- /dev/null +++ b/.github/workflows/release-desktop.yml @@ -0,0 +1,158 @@ +name: Desktop release + +on: + pull_request: + branches: [main] + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: desktop-release-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - name: macOS Apple Silicon + os: macos-14 + platform: macos + - name: Windows x64 + os: windows-latest + platform: windows + - name: Linux x86_64 + os: ubuntu-22.04 + platform: linux + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - uses: dtolnay/rust-toolchain@stable + + - name: Install Linux desktop dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + curl \ + file \ + libappindicator3-dev \ + librsvg2-dev \ + libssl-dev \ + libwebkit2gtk-4.1-dev \ + patchelf \ + pkg-config \ + wget + + - name: Install JavaScript dependencies + run: npm ci + + - name: Build pinned SilverScript compilers + run: npm run setup:silverc + + - name: Build pinned local preflight engine + run: npm run setup:kascov-preflight + + - name: Verify application and native helpers + run: npm run verify + + - name: Build macOS DMG + if: matrix.platform == 'macos' + env: + APPLE_SIGNING_IDENTITY: '-' + run: npm run desktop:build -- --bundles dmg + + - name: Stage macOS artifact + if: matrix.platform == 'macos' + shell: bash + run: | + version="$(node -p "require('./package.json').version")" + artifact="$(find src-tauri/target/release/bundle/dmg -name '*.dmg' -print -quit)" + test -n "$artifact" + mkdir -p release-assets + cp "$artifact" "release-assets/Kaspa-SilverScript-Studio-${version}-macos-arm64.dmg" + + - name: Build Windows installers + if: matrix.platform == 'windows' + run: npm run desktop:build -- --bundles nsis,msi + + - name: Stage Windows artifacts + if: matrix.platform == 'windows' + shell: pwsh + run: | + $version = node -p "require('./package.json').version" + $msi = Get-ChildItem "src-tauri/target/release/bundle/msi" -Filter "*.msi" | Select-Object -First 1 + $setup = Get-ChildItem "src-tauri/target/release/bundle/nsis" -Filter "*-setup.exe" | Select-Object -First 1 + if (-not $msi -or -not $setup) { throw "Windows MSI or NSIS installer is missing" } + New-Item -ItemType Directory -Force -Path "release-assets" | Out-Null + Copy-Item $msi.FullName "release-assets/Kaspa-SilverScript-Studio-$version-windows-x64.msi" + Copy-Item $setup.FullName "release-assets/Kaspa-SilverScript-Studio-$version-windows-x64-setup.exe" + + - name: Build Linux packages + if: matrix.platform == 'linux' + run: npm run desktop:build -- --bundles deb,appimage + + - name: Stage Linux artifacts + if: matrix.platform == 'linux' + shell: bash + run: | + version="$(node -p "require('./package.json').version")" + deb="$(find src-tauri/target/release/bundle/deb -name '*.deb' -print -quit)" + appimage="$(find src-tauri/target/release/bundle/appimage -name '*.AppImage' -print -quit)" + test -n "$deb" + test -n "$appimage" + mkdir -p release-assets + cp "$deb" "release-assets/Kaspa-SilverScript-Studio-${version}-linux-amd64.deb" + cp "$appimage" "release-assets/Kaspa-SilverScript-Studio-${version}-linux-x86_64.AppImage" + chmod +x "release-assets/Kaspa-SilverScript-Studio-${version}-linux-x86_64.AppImage" + + - name: Record artifact hashes + run: node scripts/write-release-checksums.mjs release-assets "${{ matrix.platform }}" + + - uses: actions/upload-artifact@v4 + with: + name: desktop-${{ matrix.platform }} + path: release-assets/* + if-no-files-found: error + + release: + name: Publish GitHub Release + if: startsWith(github.ref, 'refs/tags/') + needs: build + runs-on: ubuntu-22.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + pattern: desktop-* + path: release-assets + merge-multiple: true + + - name: Create release and upload native packages + env: + GH_TOKEN: ${{ github.token }} + run: | + notes="docs/releases/${GITHUB_REF_NAME}.md" + test -f "$notes" + gh release create "$GITHUB_REF_NAME" release-assets/* \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --latest \ + --title "Kaspa SilverScript Studio ${GITHUB_REF_NAME}" \ + --notes-file "$notes" diff --git a/CHANGELOG.md b/CHANGELOG.md index 703a72e..9aefe03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.2.1 — 2026-08-07 + +- Added native Windows x64 and Linux x86_64 desktop build and release pipelines alongside macOS Apple Silicon. +- Replaced Bash-only helper builds with portable Node.js scripts and added Windows `.exe` runtime wiring for `silverc` and `kascov-preflight`. +- Removed the installed application's Python dependency by porting heuristic contract triage to built-in JavaScript. +- Added target-OS tests, native helper verification, platform packages, and SHA-256 release manifests. + +## 0.2.1 — 2026-08-07(中文) + +- 在 macOS Apple Silicon 之外增加 Windows x64 与 Linux x86_64 原生桌面构建和发布流程。 +- 将仅支持 Bash 的辅助工具构建改为跨平台 Node.js 脚本,并补齐 Windows 下 `silverc` 与 `kascov-preflight` 的 `.exe` 运行时路径。 +- 将轻量契约扫描移植到内置 JavaScript,移除安装后应用对 Python 的依赖。 +- 增加目标操作系统测试、原生辅助工具验证、平台安装包和 SHA-256 发布清单。 + ## 0.2.0 — 2026-08-06 - Upgraded the default pinned official SilverScript compiler to `4b0e1cd69739934f92c3ac4df1bb13d912418b2b`; retained `2a3961c` as a SHA-256-pinned legacy reproducibility profile. diff --git a/README.md b/README.md index f01f53e..6c8b309 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ cd kaspa-silverscript-studio npm ci ``` -首次运行需要在当前系统构建固定版本的两个原生工具: +首次运行需要在当前系统构建固定版本的两个原生工具。以下命令由 Node.js 驱动,可直接用于 macOS、Windows 和 Linux: ```bash npm run setup:silverc @@ -149,9 +149,9 @@ npm run desktop:build - 固定版本本地预检引擎。 - Kaspa WASM、契约模板、知识库和第三方许可证。 -原生安装包应在对应系统上构建和测试。macOS、Windows 和 Linux 的二进制不能互相替代,公开发行前还需要完成各平台的代码签名。当前 GitHub Release 提供经过验证的 macOS Apple Silicon 构建;Windows 和 Linux 安装包在完成对应平台验证后发布。 +原生安装包必须在对应系统上构建和测试,macOS、Windows 和 Linux 的二进制不能互相替代。v0.2.1 发布流程在原生 GitHub Runner 上分别生成 macOS Apple Silicon DMG、Windows x64 NSIS/MSI,以及 Linux x86_64 DEB/AppImage,并附带 SHA-256 校验文件。 -> Windows 状态:当前原生辅助工具安装脚本仍使用 Bash 和 Unix 无后缀文件名,Windows `.exe` 路径尚未完成发行验证。完成对应修复和 Windows Runner 验证前,请不要把当前源码描述为已经验证的 Windows 发行版。 +公开包目前没有商业代码签名:macOS 使用 ad-hoc 签名且未公证,Windows 未进行 Authenticode 签名,Linux 未进行发行版签名。首次启动可能出现系统安全提示;请只从本仓库 Release 下载并核对 SHA-256。 ### 自动语言识别 @@ -336,7 +336,7 @@ cd kaspa-silverscript-studio npm ci ``` -Build the two pinned native tools for the current platform: +Build the two pinned native tools for the current platform. These Node.js-driven commands run directly on macOS, Windows, and Linux: ```bash npm run setup:silverc @@ -375,9 +375,9 @@ npm run desktop:build The desktop bundle contains the frontend, local Node.js sidecar, pinned `silverc`, pinned local preflight engine, Kaspa WASM, templates, knowledge resources, and third-party license notices. -Build and test native installers on their target operating systems. macOS, Windows, and Linux binaries are not interchangeable, and public releases require platform-appropriate code signing. The current GitHub Release contains a verified macOS Apple Silicon build; Windows and Linux installers will be published after target-platform verification. +Native installers must be built and tested on their target operating systems; macOS, Windows, and Linux binaries are not interchangeable. The v0.2.1 release pipeline produces a macOS Apple Silicon DMG, Windows x64 NSIS/MSI packages, and Linux x86_64 DEB/AppImage packages on native GitHub Runners, with SHA-256 checksum files. -> Windows status: the current native-helper setup scripts still use Bash and extensionless Unix paths. Windows `.exe` wiring has not completed release validation. Do not present the current source as a verified Windows release until that adaptation and Windows Runner verification are complete. +The public packages are not commercially code-signed: macOS is ad-hoc signed and not notarized, Windows is not Authenticode-signed, and Linux is not distribution-signed. The operating system may show a warning on first launch. Download only from this repository's Releases and verify the SHA-256 checksum. ### Automatic language selection diff --git a/docs/releases/v0.2.1.md b/docs/releases/v0.2.1.md new file mode 100644 index 0000000..f98e5a4 --- /dev/null +++ b/docs/releases/v0.2.1.md @@ -0,0 +1,51 @@ +## Kaspa SilverScript Studio v0.2.1 + +This cross-platform release adds native Windows x64 and Linux x86_64 packages while retaining the macOS Apple Silicon build. + +### Cross-platform changes + +- Replaced Bash-only native-helper setup with Node.js build scripts for macOS, Windows, and Linux. +- Added native `.exe` discovery and runtime wiring for both pinned SilverScript compilers and the local Kascov preflight engine on Windows. +- Added explicit preflight-engine injection to the Tauri sidecar environment. +- Replaced the Python-dependent heuristic contract scan with an equivalent built-in JavaScript implementation, so installed desktop builds do not require Python. +- Added native GitHub Runner builds and verification for macOS Apple Silicon, Windows x64, and Linux x86_64. + +### Packages + +- macOS Apple Silicon: DMG, ad-hoc signed and not Apple-notarized. +- Windows x64: NSIS setup executable and MSI package, not Authenticode-signed. +- Linux x86_64: DEB package and AppImage, not distribution-signed. +- SHA-256 checksum files are included for all platforms. + +### Verification boundary + +Each package is built on its target operating system only after syntax checks, automated tests, production frontend compilation, pinned native-helper compilation, and realistic SilverScript template compilation succeed. + +SilverScript and the KCC721 pack remain experimental. TN10 is recommended. KCC721 standalone and mainnet deployment remain blocked. Passing compilation and preflight tests is not an independent security audit. + +--- + +## 中文说明 + +本次跨平台版本在保留 macOS Apple Silicon 构建的同时,增加 Windows x64 和 Linux x86_64 原生安装包。 + +### 跨平台改进 + +- 将仅支持 Bash 的原生辅助工具构建流程改为兼容 macOS、Windows 和 Linux 的 Node.js 脚本。 +- 为 Windows 补齐两个固定版本 SilverScript 编译器和本地 Kascov 预检引擎的 `.exe` 识别与运行时路径。 +- Tauri sidecar 现在会显式传入本地预检引擎路径。 +- 将依赖 Python 的轻量契约扫描改为等价的内置 JavaScript 实现,安装后的桌面应用不再要求用户安装 Python。 +- 增加 macOS Apple Silicon、Windows x64 和 Linux x86_64 原生 GitHub Runner 构建与验证。 + +### 安装包 + +- macOS Apple Silicon:DMG,采用 ad-hoc 签名,未进行 Apple 公证。 +- Windows x64:NSIS 安装程序和 MSI,未进行 Authenticode 签名。 +- Linux x86_64:DEB 和 AppImage,未进行发行版签名。 +- 三个平台均提供 SHA-256 校验文件。 + +### 验证边界 + +每个平台仅在语法检查、自动化测试、前端生产构建、固定原生辅助工具编译和真实 SilverScript 模板编译全部成功后生成安装包。 + +SilverScript 与 KCC721 实验包仍处于实验阶段,推荐使用 TN10。KCC721 普通单合约部署和主网部署仍保持禁用。成功编译和预检测试不等于独立安全审计。 diff --git a/package-lock.json b/package-lock.json index a3bc984..83d168b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kaspa-silverscript-studio", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kaspa-silverscript-studio", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "@kluster/kaspa-wasm": "2.0.1", diff --git a/package.json b/package.json index 2d34f3c..7ec43af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kaspa-silverscript-studio", - "version": "0.2.0", + "version": "0.2.1", "private": true, "type": "module", "description": "Local bilingual AI-assisted SilverScript contract studio for Kaspa", @@ -10,8 +10,8 @@ "start": "node server/index.mjs", "check": "node scripts/check-syntax.mjs", "test": "node --test", - "setup:silverc": "bash scripts/build-silverc.sh", - "setup:kascov-preflight": "bash scripts/build-kascov-preflight.sh", + "setup:silverc": "node scripts/build-silverc.mjs", + "setup:kascov-preflight": "node scripts/build-kascov-preflight.mjs", "verify": "npm run check && npm test && npm run build", "desktop:prepare": "npm run build && node scripts/prepare-desktop-runtime.mjs", "desktop:dev": "tauri dev", diff --git a/scripts/build-kascov-preflight.mjs b/scripts/build-kascov-preflight.mjs new file mode 100644 index 0000000..cf97081 --- /dev/null +++ b/scripts/build-kascov-preflight.mjs @@ -0,0 +1,41 @@ +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +import { cargoReleaseBinary, executableName, makeExecutable } from "./platform-binaries.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const repositoryUrl = process.env.KASCOV_REPOSITORY || "https://github.com/Knitser/kascov.git"; +const commit = process.env.KASCOV_COMMIT || "b64d6b4114df324f899783080371f26b619b19d0"; +const work = path.resolve(process.env.KASCOV_BUILD_DIR || path.join(root, ".build", `kascov-${commit}`)); +const targetDirectory = path.resolve(process.env.CARGO_TARGET_DIR || path.join(root, ".build", "kascov-target")); +const output = path.join(root, "bin", executableName("kascov-preflight")); + +function run(command, args, options = {}) { + execFileSync(command, args, { stdio: "inherit", ...options }); +} + +if (!fs.existsSync(path.join(work, ".git"))) { + fs.rmSync(work, { recursive: true, force: true }); + fs.mkdirSync(path.dirname(work), { recursive: true }); + run("git", ["clone", "--filter=blob:none", "--no-checkout", repositoryUrl, work]); +} + +run("git", ["-C", work, "fetch", "--depth", "1", "origin", commit]); +run("git", ["-C", work, "checkout", "--detach", "--force", commit]); +const sourceDirectory = path.join(work, "crates", "kascov", "src", "bin"); +fs.mkdirSync(sourceDirectory, { recursive: true }); +fs.copyFileSync(path.join(root, "native", "kascov-preflight-main.rs"), path.join(sourceDirectory, "kascov-preflight.rs")); +run("cargo", [ + "build", + "--manifest-path", path.join(work, "Cargo.toml"), + "--locked", + "--release", + "-p", "kascov", + "--bin", "kascov-preflight" +], { env: { ...process.env, CARGO_TARGET_DIR: targetDirectory } }); +fs.mkdirSync(path.dirname(output), { recursive: true }); +fs.copyFileSync(cargoReleaseBinary(targetDirectory, "kascov-preflight"), output); +makeExecutable(output); +run(process.execPath, [path.join(root, "scripts", "write-kascov-preflight-manifest.mjs")]); diff --git a/scripts/build-kascov-preflight.sh b/scripts/build-kascov-preflight.sh index 490f020..3f2e219 100755 --- a/scripts/build-kascov-preflight.sh +++ b/scripts/build-kascov-preflight.sh @@ -1,31 +1,5 @@ #!/usr/bin/env bash set -euo pipefail -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -KASCOV_REPOSITORY="${KASCOV_REPOSITORY:-https://github.com/Knitser/kascov.git}" -KASCOV_COMMIT="${KASCOV_COMMIT:-b64d6b4114df324f899783080371f26b619b19d0}" -WORK="${KASCOV_BUILD_DIR:-$ROOT/.build/kascov-$KASCOV_COMMIT}" -TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/.build/kascov-target}" -OUTPUT="$ROOT/bin/kascov-preflight" - -if [ ! -d "$WORK/.git" ]; then - rm -rf "$WORK" - mkdir -p "$(dirname "$WORK")" - git clone --filter=blob:none --no-checkout "$KASCOV_REPOSITORY" "$WORK" -fi - -git -C "$WORK" fetch --depth 1 origin "$KASCOV_COMMIT" -git -C "$WORK" checkout --detach --force "$KASCOV_COMMIT" -mkdir -p "$WORK/crates/kascov/src/bin" -cp "$ROOT/native/kascov-preflight-main.rs" "$WORK/crates/kascov/src/bin/kascov-preflight.rs" - -CARGO_TARGET_DIR="$TARGET_DIR" cargo build \ - --manifest-path "$WORK/Cargo.toml" \ - --locked \ - --release \ - -p kascov \ - --bin kascov-preflight - -cp "$TARGET_DIR/release/kascov-preflight" "$OUTPUT" -chmod 755 "$OUTPUT" -node "$ROOT/scripts/write-kascov-preflight-manifest.mjs" +root="$(cd "$(dirname "$0")/.." && pwd)" +exec node "$root/scripts/build-kascov-preflight.mjs" diff --git a/scripts/build-silverc.mjs b/scripts/build-silverc.mjs new file mode 100644 index 0000000..d6c40de --- /dev/null +++ b/scripts/build-silverc.mjs @@ -0,0 +1,98 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +import { cargoReleaseBinary, executableName, makeExecutable } from "./platform-binaries.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const latestCommit = "4b0e1cd69739934f92c3ac4df1bb13d912418b2b"; +const legacyCommit = "2a3961cadc76bb16a425042172ffe32481da89b5"; +const work = fs.mkdtempSync(path.join(os.tmpdir(), "silverstudio-silverc-")); + +function run(command, args, options = {}) { + execFileSync(command, args, { stdio: "inherit", ...options }); +} + +function output(command, args, options = {}) { + return execFileSync(command, args, { encoding: "utf8", ...options }).trim(); +} + +function sha256(file) { + return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +} + +function buildProfile({ id, commit, outputName, configuredSource }) { + let repository = path.join(work, id); + if (configuredSource) { + repository = path.resolve(configuredSource); + const actual = output("git", ["-C", repository, "rev-parse", "HEAD"]); + if (actual !== commit) throw new Error(`Local SilverScript source for ${id} is at ${actual}, expected ${commit}`); + } else { + run("git", ["clone", "--filter=blob:none", "--no-checkout", "https://github.com/kaspanet/silverscript.git", repository]); + run("git", ["-C", repository, "checkout", "--detach", commit]); + } + + const targetDirectory = path.join(work, `target-${id}`); + run("cargo", [ + "build", + "--manifest-path", path.join(repository, "Cargo.toml"), + "-p", "silverscript-lang", + "--bin", "silverc", + "--release" + ], { env: { ...process.env, CARGO_TARGET_DIR: targetDirectory } }); + + const source = cargoReleaseBinary(targetDirectory, "silverc"); + const destination = path.join(root, "bin", executableName(outputName)); + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.copyFileSync(source, destination); + makeExecutable(destination); + return destination; +} + +try { + fs.mkdirSync(path.join(root, "config"), { recursive: true }); + const latestBin = buildProfile({ + id: "latest-4b0e1cd", + commit: latestCommit, + outputName: "silverc-latest", + configuredSource: process.env.SILVERSCRIPT_LATEST_SOURCE || process.env.SILVERSCRIPT_SOURCE || "" + }); + const legacyBin = buildProfile({ + id: "legacy-2a3961c", + commit: legacyCommit, + outputName: "silverc-legacy", + configuredSource: process.env.SILVERSCRIPT_LEGACY_SOURCE || "" + }); + const latestSha256 = sha256(latestBin); + const legacySha256 = sha256(legacyBin); + const manifest = { + defaultProfileId: "latest-4b0e1cd", + profiles: { + "latest-4b0e1cd": { + bin: latestBin, + sha256: latestSha256, + upstreamCommit: latestCommit, + builtAt: new Date().toISOString() + }, + "legacy-2a3961c": { + bin: legacyBin, + sha256: legacySha256, + upstreamCommit: legacyCommit, + builtAt: new Date().toISOString() + } + } + }; + fs.writeFileSync(path.join(root, "config", "compiler.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 }); + run(latestBin, ["--help"]); + run(legacyBin, ["--help"]); + console.log(`latest silverc commit: ${latestCommit}`); + console.log(`latest silverc sha256: ${latestSha256}`); + console.log(`legacy silverc commit: ${legacyCommit}`); + console.log(`legacy silverc sha256: ${legacySha256}`); + console.log(`manifest: ${path.join(root, "config", "compiler.json")}`); +} finally { + fs.rmSync(work, { recursive: true, force: true }); +} diff --git a/scripts/build-silverc.sh b/scripts/build-silverc.sh index 55c581d..c6da556 100644 --- a/scripts/build-silverc.sh +++ b/scripts/build-silverc.sh @@ -2,66 +2,4 @@ set -euo pipefail root="$(cd "$(dirname "$0")/.." && pwd)" -latest_commit="4b0e1cd69739934f92c3ac4df1bb13d912418b2b" -legacy_commit="2a3961cadc76bb16a425042172ffe32481da89b5" -work="$(mktemp -d "${TMPDIR:-/tmp}/silverstudio-silverc.XXXXXX")" -mkdir -p "$root/bin" "$root/config" - -build_profile() { - local id="$1" - local commit="$2" - local output="$3" - local configured_source="$4" - local repo="$work/$id" - local target="$work/target-$id" - if [[ -n "$configured_source" ]]; then - repo="$(cd "$configured_source" && pwd)" - local actual - actual="$(git -C "$repo" rev-parse HEAD)" - if [[ "$actual" != "$commit" ]]; then - printf 'local SilverScript source for %s is at %s, expected %s\n' "$id" "$actual" "$commit" >&2 - exit 1 - fi - else - git clone --filter=blob:none --no-checkout https://github.com/kaspanet/silverscript.git "$repo" - git -C "$repo" checkout --detach "$commit" - fi - CARGO_TARGET_DIR="$target" cargo build --manifest-path "$repo/Cargo.toml" -p silverscript-lang --bin silverc --release - install -m 0755 "$target/release/silverc" "$root/bin/$output" -} - -build_profile "latest-4b0e1cd" "$latest_commit" "silverc-latest" "${SILVERSCRIPT_LATEST_SOURCE:-${SILVERSCRIPT_SOURCE:-}}" -build_profile "legacy-2a3961c" "$legacy_commit" "silverc-legacy" "${SILVERSCRIPT_LEGACY_SOURCE:-}" - -latest_sha="$(shasum -a 256 "$root/bin/silverc-latest" | awk '{print $1}')" -legacy_sha="$(shasum -a 256 "$root/bin/silverc-legacy" | awk '{print $1}')" -node - "$root" "$latest_commit" "$latest_sha" "$legacy_commit" "$legacy_sha" <<'NODE' -const fs = require("node:fs"); -const path = require("node:path"); -const [root, latestCommit, latestSha256, legacyCommit, legacySha256] = process.argv.slice(2); -fs.writeFileSync(path.join(root, "config", "compiler.json"), `${JSON.stringify({ - defaultProfileId: "latest-4b0e1cd", - profiles: { - "latest-4b0e1cd": { - bin: path.join(root, "bin", "silverc-latest"), - sha256: latestSha256, - upstreamCommit: latestCommit, - builtAt: new Date().toISOString() - }, - "legacy-2a3961c": { - bin: path.join(root, "bin", "silverc-legacy"), - sha256: legacySha256, - upstreamCommit: legacyCommit, - builtAt: new Date().toISOString() - } - } -}, null, 2)}\n`, { mode: 0o600 }); -NODE - -"$root/bin/silverc-latest" --help >/dev/null -"$root/bin/silverc-legacy" --help >/dev/null -printf 'latest silverc commit: %s\n' "$latest_commit" -printf 'latest silverc sha256: %s\n' "$latest_sha" -printf 'legacy silverc commit: %s\n' "$legacy_commit" -printf 'legacy silverc sha256: %s\n' "$legacy_sha" -printf 'manifest: %s\n' "$root/config/compiler.json" +exec node "$root/scripts/build-silverc.mjs" diff --git a/scripts/check-syntax.mjs b/scripts/check-syntax.mjs index 0c35f07..a26a415 100644 --- a/scripts/check-syntax.mjs +++ b/scripts/check-syntax.mjs @@ -18,7 +18,7 @@ const preflightManifest = JSON.parse(fs.readFileSync(path.join(root, "config", " const preflightLocalManifestFile = path.join(root, "config", "kascov-preflight.local.json"); if (!fs.existsSync(preflightLocalManifestFile)) throw new Error("Local preflight manifest is missing. Run npm run setup:kascov-preflight."); const preflightLocalManifest = JSON.parse(fs.readFileSync(preflightLocalManifestFile, "utf8")); -const preflightBinary = path.join(root, preflightManifest.binary); +const preflightBinary = path.resolve(root, preflightLocalManifest.binary || preflightManifest.binary); if (!fs.existsSync(preflightBinary)) throw new Error("Pinned local preflight engine is missing. Run npm run setup:kascov-preflight."); const preflightSha256 = crypto.createHash("sha256").update(fs.readFileSync(preflightBinary)).digest("hex"); if (preflightLocalManifest.upstreamCommit !== preflightManifest.upstreamCommit) throw new Error("Local preflight manifest was built from a different Kascov commit"); diff --git a/scripts/platform-binaries.mjs b/scripts/platform-binaries.mjs new file mode 100644 index 0000000..8586440 --- /dev/null +++ b/scripts/platform-binaries.mjs @@ -0,0 +1,22 @@ +import fs from "node:fs"; +import path from "node:path"; + +export function executableSuffix(platform = process.platform) { + return platform === "win32" ? ".exe" : ""; +} + +export function executableName(name, platform = process.platform) { + return `${name}${executableSuffix(platform)}`; +} + +export function binaryRelativePath(name, platform = process.platform) { + return path.posix.join("bin", executableName(name, platform)); +} + +export function cargoReleaseBinary(targetDirectory, name, platform = process.platform) { + return path.join(targetDirectory, "release", executableName(name, platform)); +} + +export function makeExecutable(file, platform = process.platform) { + if (platform !== "win32") fs.chmodSync(file, 0o755); +} diff --git a/scripts/prepare-desktop-runtime.mjs b/scripts/prepare-desktop-runtime.mjs index 63824d6..f6884b5 100644 --- a/scripts/prepare-desktop-runtime.mjs +++ b/scripts/prepare-desktop-runtime.mjs @@ -3,6 +3,8 @@ import path from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { binaryRelativePath, executableName, makeExecutable } from "./platform-binaries.mjs"; + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const tauri = path.join(root, "src-tauri"); const runtime = path.join(tauri, "runtime", "app"); @@ -14,25 +16,29 @@ function copy(relative) { fs.cpSync(path.join(root, relative), destination, { recursive: true, force: true }); } -for (const compiler of ["silverc-latest", "silverc-legacy"]) { - if (!fs.existsSync(path.join(root, "bin", compiler))) throw new Error(`Pinned ${compiler} is missing. Run npm run setup:silverc first.`); +const helperNames = ["silverc-latest", "silverc-legacy", "kascov-preflight"]; +for (const helper of helperNames) { + const source = path.join(root, binaryRelativePath(helper)); + if (!fs.existsSync(source)) throw new Error(`Pinned ${executableName(helper)} is missing. Run the matching setup command first.`); } -if (!fs.existsSync(path.join(root, "bin", "kascov-preflight"))) throw new Error("Pinned local preflight engine is missing. Run npm run setup:kascov-preflight first."); fs.rmSync(path.join(tauri, "runtime"), { recursive: true, force: true }); fs.mkdirSync(runtime, { recursive: true }); -for (const entry of ["server", "src/kcc721-metadata.js", "templates", "knowledge", "config", "third_party", "bin", "dist", "node_modules", "package.json"]) copy(entry); +for (const entry of ["server", "src/kcc721-metadata.js", "templates", "knowledge", "config", "third_party", "dist", "node_modules", "package.json"]) copy(entry); +fs.mkdirSync(path.join(runtime, "bin"), { recursive: true }); +for (const helper of helperNames) { + const relative = binaryRelativePath(helper); + const destination = path.join(runtime, relative); + fs.copyFileSync(path.join(root, relative), destination); + makeExecutable(destination); +} const hostLine = execFileSync("rustc", ["-vV"], { encoding: "utf8" }).split("\n").find((line) => line.startsWith("host: ")); if (!hostLine) throw new Error("Unable to determine the Rust target triple"); const triple = hostLine.slice(6).trim(); fs.mkdirSync(binaries, { recursive: true }); -const extension = process.platform === "win32" ? ".exe" : ""; -const sidecar = path.join(binaries, `node-${triple}${extension}`); +const sidecar = path.join(binaries, `node-${triple}${process.platform === "win32" ? ".exe" : ""}`); fs.copyFileSync(process.execPath, sidecar); -fs.chmodSync(sidecar, 0o755); -fs.chmodSync(path.join(runtime, "bin", "silverc-latest"), 0o755); -fs.chmodSync(path.join(runtime, "bin", "silverc-legacy"), 0o755); -fs.chmodSync(path.join(runtime, "bin", "kascov-preflight"), 0o755); +makeExecutable(sidecar); console.log(`Desktop runtime prepared for ${triple}`); console.log(`Node sidecar: ${sidecar}`); diff --git a/scripts/write-kascov-preflight-manifest.mjs b/scripts/write-kascov-preflight-manifest.mjs index 3dc4aa6..da05097 100644 --- a/scripts/write-kascov-preflight-manifest.mjs +++ b/scripts/write-kascov-preflight-manifest.mjs @@ -3,16 +3,19 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { binaryRelativePath } from "./platform-binaries.mjs"; + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const provenanceFile = path.join(root, "config", "kascov-preflight.json"); const localFile = path.join(root, "config", "kascov-preflight.local.json"); const provenance = JSON.parse(fs.readFileSync(provenanceFile, "utf8")); -const binary = path.resolve(root, provenance.binary); +const relativeBinary = binaryRelativePath("kascov-preflight"); +const binary = path.resolve(root, relativeBinary); if (!fs.existsSync(binary)) throw new Error(`Local preflight binary is missing: ${binary}`); const sha256 = crypto.createHash("sha256").update(fs.readFileSync(binary)).digest("hex"); const local = { - binary: provenance.binary, + binary: relativeBinary, sha256, platform: process.platform, architecture: process.arch, diff --git a/scripts/write-release-checksums.mjs b/scripts/write-release-checksums.mjs new file mode 100644 index 0000000..464ca01 --- /dev/null +++ b/scripts/write-release-checksums.mjs @@ -0,0 +1,19 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; + +const directory = path.resolve(process.argv[2] || "release-assets"); +const platform = String(process.argv[3] || process.platform).replace(/[^a-z0-9_-]/gi, ""); +if (!fs.existsSync(directory)) throw new Error(`Release asset directory is missing: ${directory}`); +const outputName = `SHA256SUMS-${platform}.txt`; +const files = fs.readdirSync(directory, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name !== outputName) + .map((entry) => entry.name) + .sort(); +if (!files.length) throw new Error(`No release assets found in ${directory}`); +const lines = files.map((name) => { + const digest = crypto.createHash("sha256").update(fs.readFileSync(path.join(directory, name))).digest("hex"); + return `${digest} ${name}`; +}); +fs.writeFileSync(path.join(directory, outputName), `${lines.join("\n")}\n`); +console.log(lines.join("\n")); diff --git a/server/compiler.mjs b/server/compiler.mjs index cbc2691..a0a825d 100644 --- a/server/compiler.mjs +++ b/server/compiler.mjs @@ -181,27 +181,61 @@ export function migrateSourceToProfile(source, targetProfileId = config.compiler }; } +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + export async function staticAnalyze(source) { const text = boundedText(source, "contract source"); - const directory = fs.mkdtempSync(path.join(os.tmpdir(), "silverstudio-audit-")); - const sourceFile = path.join(directory, "contract.sil"); - try { - fs.writeFileSync(sourceFile, text, { mode: 0o600 }); - const script = path.join(config.root, "knowledge", "kaspa-silverscript", "scripts", "audit_silverscript.py"); - const { stdout } = await execFileAsync("python3", [script, sourceFile], { timeout: 20_000, maxBuffer: 2_000_000 }); - const findings = String(stdout || "").split("\n").map((line) => { - const match = line.match(/:(\d+):\s+(SS\d+):\s+(.+)$/); - return match ? { line: Number(match[1]), code: match[2], message: match[3] } : null; - }).filter(Boolean); - return { - kind: "heuristic-triage", - findings, - findingCount: findings.length, - note: "Static pattern triage only; successful output is not compilation or a security proof." - }; - } finally { - fs.rmSync(directory, { recursive: true, force: true }); + const findings = []; + const line = (offset) => text.slice(0, offset).split("\n").length; + const crossTemplate = /validateOutputStateWithTemplate\s*\(\s*([^,\n]+)/g; + let match; + while ((match = crossTemplate.exec(text))) { + const argument = match[1].trim(); + const escaped = escapeRegExp(argument); + const direct = argument.includes("OpCovOutputIdx"); + const idGuard = new RegExp(`OpOutputCovenantId\\s*\\(\\s*${escaped}\\s*\\)`).test(text); + const derived = new RegExp(`\\b${escaped}\\s*=\\s*OpCovOutputIdx\\s*\\(`).test(text); + if (!direct && !idGuard && !derived) { + findings.push({ + code: "SS001", + line: line(match.index), + message: `cross-template output '${argument}' is not visibly bound to a covenant ID` + }); + } + } + const scriptComparison = /scriptPubKey\s*==\s*(?!byte\[\]\s*\()([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)/g; + while ((match = scriptComparison.exec(text))) { + findings.push({ + code: "SS002", + line: line(match.index), + message: `scriptPubKey comparison with '${match[1]}' may mix byte[] and fixed bytes` + }); + } + if (text.includes("termination = allowed") + && /return\s*\(?\s*next_states\s*\)?\s*;/.test(text) + && !/next_states\.length\s*==\s*0/.test(text)) { + findings.push({ + code: "SS003", + line: line(text.indexOf("termination = allowed")), + message: "termination path returns caller-supplied state without requiring termination" + }); } + const feeOutput = /tx\.outputs\s*\[\s*fee\w*Index\s*\]/i.exec(text); + if (feeOutput) { + findings.push({ + code: "SS004", + line: line(feeOutput.index), + message: "review fee-output aliasing across multiple contract executions" + }); + } + return { + kind: "heuristic-triage", + findings, + findingCount: findings.length, + note: "Static pattern triage only; successful output is not compilation or a security proof." + }; } export async function compileContract({ source, constructorArgs = [], compilerProfileId = config.compiler.defaultProfileId }) { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d344eac..3ed1f1c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -66,7 +66,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "app" -version = "0.2.0" +version = "0.2.1" dependencies = [ "log", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fe4fb0a..31c71d5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.2.0" +version = "0.2.1" description = "Local-first Kaspa SilverScript covenant workbench" authors = ["w00c00"] license = "MIT" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 795b9b7..d860e1e 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build() } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3abee17..a5ba6cd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,54 +9,61 @@ struct BackendChild(Mutex>); #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let app = tauri::Builder::default() - .plugin(tauri_plugin_shell::init()) - .manage(BackendChild::default()) - .setup(|app| { - if cfg!(debug_assertions) { - app.handle().plugin( - tauri_plugin_log::Builder::default() - .level(log::LevelFilter::Info) - .build(), - )?; - } + let app = tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .manage(BackendChild::default()) + .setup(|app| { + if cfg!(debug_assertions) { + app.handle().plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + )?; + } - if !cfg!(debug_assertions) { - let resource_dir = app.path().resource_dir()?; - let app_data_dir = app.path().app_data_dir()?; - fs::create_dir_all(&app_data_dir)?; - let script = resource_dir.join("runtime/app/server/index.mjs"); - let latest_compiler = resource_dir.join("runtime/app/bin/silverc-latest"); - let legacy_compiler = resource_dir.join("runtime/app/bin/silverc-legacy"); - let (mut events, child) = app - .shell() - .sidecar("node")? - .arg(script) - .env("HOST", "127.0.0.1") - .env("PORT", "4310") - .env("STUDIO_DATA_DIR", app_data_dir) - .env("SILVERC_LATEST_BIN", latest_compiler) - .env("SILVERC_LEGACY_BIN", legacy_compiler) - .spawn()?; - *app.state::().0.lock().unwrap() = Some(child); - tauri::async_runtime::spawn(async move { - while let Some(event) = events.recv().await { - if let tauri_plugin_shell::process::CommandEvent::Stderr(bytes) = event { - log::error!("studio sidecar: {}", String::from_utf8_lossy(&bytes)); + if !cfg!(debug_assertions) { + let resource_dir = app.path().resource_dir()?; + let app_data_dir = app.path().app_data_dir()?; + fs::create_dir_all(&app_data_dir)?; + let executable_suffix = if cfg!(windows) { ".exe" } else { "" }; + let script = resource_dir.join("runtime/app/server/index.mjs"); + let latest_compiler = + resource_dir.join(format!("runtime/app/bin/silverc-latest{executable_suffix}")); + let legacy_compiler = + resource_dir.join(format!("runtime/app/bin/silverc-legacy{executable_suffix}")); + let preflight_engine = resource_dir.join(format!( + "runtime/app/bin/kascov-preflight{executable_suffix}" + )); + let (mut events, child) = app + .shell() + .sidecar("node")? + .arg(script) + .env("HOST", "127.0.0.1") + .env("PORT", "4310") + .env("STUDIO_DATA_DIR", app_data_dir) + .env("SILVERC_LATEST_BIN", latest_compiler) + .env("SILVERC_LEGACY_BIN", legacy_compiler) + .env("KASCOV_PREFLIGHT_BIN", preflight_engine) + .spawn()?; + *app.state::().0.lock().unwrap() = Some(child); + tauri::async_runtime::spawn(async move { + while let Some(event) = events.recv().await { + if let tauri_plugin_shell::process::CommandEvent::Stderr(bytes) = event { + log::error!("studio sidecar: {}", String::from_utf8_lossy(&bytes)); + } + } + }); } - } - }); - } - Ok(()) - }) - .build(tauri::generate_context!()) - .expect("error while building tauri application"); + Ok(()) + }) + .build(tauri::generate_context!()) + .expect("error while building tauri application"); - app.run(|handle, event| { - if let tauri::RunEvent::Exit = event { - if let Some(child) = handle.state::().0.lock().unwrap().take() { - let _ = child.kill(); - } - } - }); + app.run(|handle, event| { + if let tauri::RunEvent::Exit = event { + if let Some(child) = handle.state::().0.lock().unwrap().take() { + let _ = child.kill(); + } + } + }); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ad5fe83..69c3a72 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - app_lib::run(); + app_lib::run(); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 124dae3..9e8ffae 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Kaspa SilverScript Studio", - "version": "0.2.0", + "version": "0.2.1", "identifier": "io.kaspa.silverscript-studio", "build": { "frontendDist": "../dist", diff --git a/test/studio.test.mjs b/test/studio.test.mjs index d04e5d2..6146af1 100644 --- a/test/studio.test.mjs +++ b/test/studio.test.mjs @@ -27,6 +27,7 @@ import { CovenantStateSource, covenantStateProvider, verifyCovenantStateCandidat import { createP2pkCoSpendAuthorization, selectP2pkFundingUtxo, signP2pkCoSpendPackage } from "../server/p2pk-cospend.mjs"; import { buildAtomicCovenantPackage } from "../server/atomic-covenant-builder.mjs"; import { canonicalKcc721Metadata } from "../src/kcc721-metadata.js"; +import { binaryRelativePath, cargoReleaseBinary, executableName } from "../scripts/platform-binaries.mjs"; const require = createRequire(import.meta.url); const kaspa = require("@kluster/kaspa-wasm"); @@ -100,6 +101,17 @@ test("desktop runtime includes server-imported KCC721 metadata code", () => { assert.match(prepareDesktop, /src\/kcc721-metadata\.js/); }); +test("desktop helpers use native executable names on Windows and Unix", () => { + assert.equal(executableName("silverc-latest", "win32"), "silverc-latest.exe"); + assert.equal(executableName("silverc-latest", "linux"), "silverc-latest"); + assert.equal(binaryRelativePath("kascov-preflight", "win32"), "bin/kascov-preflight.exe"); + assert.equal(binaryRelativePath("kascov-preflight", "darwin"), "bin/kascov-preflight"); + assert.equal(cargoReleaseBinary("target", "silverc", "win32"), path.join("target", "release", "silverc.exe")); + const rustLauncher = fs.readFileSync(new URL("../src-tauri/src/lib.rs", import.meta.url), "utf8"); + assert.match(rustLauncher, /KASCOV_PREFLIGHT_BIN/); + assert.match(rustLauncher, /cfg!\(windows\)/); +}); + test("AI package retains explicit transaction plans and stays experimental", () => { const parsed = parseAiContract(JSON.stringify({ specification: { title: "Counter" }, @@ -1132,6 +1144,15 @@ test("heuristic triage returns structured findings", async () => { const analysis = await staticAnalyze(SAMPLE_SOURCE); assert.equal(analysis.kind, "heuristic-triage"); assert.ok(Array.isArray(analysis.findings)); + const risky = await staticAnalyze(`pragma silverscript ^0.1.0; +contract Risky() { + policy spend(...) -> (next_states: State[]) termination = allowed { + validateOutputStateWithTemplate(outputIndex, template); + require(tx.outputs[feeOutputIndex].scriptPubKey == expectedScript); + return next_states; + } +}`); + assert.deepEqual(risky.findings.map((finding) => finding.code), ["SS001", "SS002", "SS003", "SS004"]); }); test("deployment builder rejects source edited after compilation before network access", async () => { From 4115d81cfd5fb66421877c020f77ba591e8be3bf Mon Sep 17 00:00:00 2001 From: w00c00 <46839097+w00c00@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:26:10 +0800 Subject: [PATCH 2/4] Build minimal cross-platform preflight engine --- native/kascov-preflight-main.rs | 34 ++++++++++++++++-- scripts/build-kascov-preflight.mjs | 57 ++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/native/kascov-preflight-main.rs b/native/kascov-preflight-main.rs index 21128c1..95c44c9 100644 --- a/native/kascov-preflight-main.rs +++ b/native/kascov-preflight-main.rs @@ -5,13 +5,43 @@ //! stdin and writes a single JSON report to stdout. It never connects to a //! node, reads keys, broadcasts, or persists input. -#[path = "../preflight.rs"] +#[path = "preflight.rs"] mod preflight; -use kascov_core::Network; +use std::fmt; use std::io::{self, Read}; use std::str::FromStr; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Network { + Mainnet, + Testnet(u32), +} + +impl fmt::Display for Network { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Mainnet => formatter.write_str("mainnet"), + Self::Testnet(suffix) => write!(formatter, "testnet-{suffix}"), + } + } +} + +impl FromStr for Network { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "mainnet" => Ok(Self::Mainnet), + _ => value + .strip_prefix("testnet-") + .and_then(|suffix| suffix.parse().ok()) + .map(Self::Testnet) + .ok_or_else(|| format!("invalid network: {value}")), + } + } +} + fn main() { if let Err(error) = run() { eprintln!("{error}"); diff --git a/scripts/build-kascov-preflight.mjs b/scripts/build-kascov-preflight.mjs index cf97081..6341e88 100644 --- a/scripts/build-kascov-preflight.mjs +++ b/scripts/build-kascov-preflight.mjs @@ -11,6 +11,7 @@ const commit = process.env.KASCOV_COMMIT || "b64d6b4114df324f899783080371f26b619 const work = path.resolve(process.env.KASCOV_BUILD_DIR || path.join(root, ".build", `kascov-${commit}`)); const targetDirectory = path.resolve(process.env.CARGO_TARGET_DIR || path.join(root, ".build", "kascov-target")); const output = path.join(root, "bin", executableName("kascov-preflight")); +const packageName = "studio-kascov-preflight"; function run(command, args, options = {}) { execFileSync(command, args, { stdio: "inherit", ...options }); @@ -24,16 +25,60 @@ if (!fs.existsSync(path.join(work, ".git"))) { run("git", ["-C", work, "fetch", "--depth", "1", "origin", commit]); run("git", ["-C", work, "checkout", "--detach", "--force", commit]); -const sourceDirectory = path.join(work, "crates", "kascov", "src", "bin"); +const lockFile = path.join(work, "Cargo.lock"); +const pinnedLock = fs.readFileSync(lockFile, "utf8"); +const crateDirectory = path.join(work, "crates", packageName); +const sourceDirectory = path.join(crateDirectory, "src"); fs.mkdirSync(sourceDirectory, { recursive: true }); -fs.copyFileSync(path.join(root, "native", "kascov-preflight-main.rs"), path.join(sourceDirectory, "kascov-preflight.rs")); -run("cargo", [ +fs.copyFileSync(path.join(root, "native", "kascov-preflight-main.rs"), path.join(sourceDirectory, "main.rs")); +const upstreamPreflightFile = path.join(work, "crates", "kascov", "src", "preflight.rs"); +const upstreamPreflight = fs.readFileSync(upstreamPreflightFile, "utf8"); +if (!upstreamPreflight.includes("use kascov_core::Network;")) throw new Error("Pinned Kascov preflight source has an unexpected Network import"); +fs.writeFileSync(path.join(sourceDirectory, "preflight.rs"), upstreamPreflight.replace("use kascov_core::Network;", "use crate::Network;")); +fs.writeFileSync(path.join(crateDirectory, "Cargo.toml"), `[package] +name = "${packageName}" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +kascov-decode = { workspace = true } +kascov-sim = { workspace = true } +kaspa-consensus-core = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +hex = { workspace = true } + +[[bin]] +name = "kascov-preflight" +path = "src/main.rs" +`); +const workspaceManifestFile = path.join(work, "Cargo.toml"); +const workspaceManifest = fs.readFileSync(workspaceManifestFile, "utf8"); +if (!workspaceManifest.includes("members = [")) throw new Error("Pinned Kascov workspace manifest has an unexpected members declaration"); +fs.writeFileSync(workspaceManifestFile, workspaceManifest.replace("members = [", `members = ["crates/${packageName}", `)); +const buildArgs = [ "build", - "--manifest-path", path.join(work, "Cargo.toml"), - "--locked", + "--manifest-path", workspaceManifestFile, "--release", - "-p", "kascov", + "-p", packageName, "--bin", "kascov-preflight" +]; +// Cargo must first register the injected local package in the upstream lockfile. +// The existing lockfile supplies every external resolution. We then remove only +// that local package stanza and require the rest to match the pinned file byte +// for byte before repeating the build under --locked. +run("cargo", buildArgs, { env: { ...process.env, CARGO_TARGET_DIR: targetDirectory } }); +const updatedLock = fs.readFileSync(lockFile, "utf8"); +const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +const localPackagePattern = new RegExp(`\\n\\[\\[package\\]\\]\\nname = "${escapedPackageName}"\\n[\\s\\S]*?(?=\\n\\[\\[package\\]\\]|\\s*$)`); +const lockWithoutLocalPackage = updatedLock.replace(localPackagePattern, ""); +if (lockWithoutLocalPackage === updatedLock) throw new Error("Cargo did not register the local Kascov preflight package"); +if (lockWithoutLocalPackage !== pinnedLock) throw new Error("Pinned Kascov dependency lockfile drifted while adding the local preflight package"); +run("cargo", [ + "build", + "--locked", + ...buildArgs.slice(1) ], { env: { ...process.env, CARGO_TARGET_DIR: targetDirectory } }); fs.mkdirSync(path.dirname(output), { recursive: true }); fs.copyFileSync(cargoReleaseBinary(targetDirectory, "kascov-preflight"), output); From e38a72e562c3d1e4242606819cd04779fb9cae7a Mon Sep 17 00:00:00 2001 From: w00c00 <46839097+w00c00@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:39:18 +0800 Subject: [PATCH 3/4] Normalize Cargo lockfile line endings --- scripts/build-kascov-preflight.mjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/build-kascov-preflight.mjs b/scripts/build-kascov-preflight.mjs index 6341e88..4b68d1a 100644 --- a/scripts/build-kascov-preflight.mjs +++ b/scripts/build-kascov-preflight.mjs @@ -72,9 +72,11 @@ run("cargo", buildArgs, { env: { ...process.env, CARGO_TARGET_DIR: targetDirecto const updatedLock = fs.readFileSync(lockFile, "utf8"); const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const localPackagePattern = new RegExp(`\\n\\[\\[package\\]\\]\\nname = "${escapedPackageName}"\\n[\\s\\S]*?(?=\\n\\[\\[package\\]\\]|\\s*$)`); -const lockWithoutLocalPackage = updatedLock.replace(localPackagePattern, ""); -if (lockWithoutLocalPackage === updatedLock) throw new Error("Cargo did not register the local Kascov preflight package"); -if (lockWithoutLocalPackage !== pinnedLock) throw new Error("Pinned Kascov dependency lockfile drifted while adding the local preflight package"); +const normalizedUpdatedLock = updatedLock.replace(/\\r\\n/g, "\\n"); +const normalizedPinnedLock = pinnedLock.replace(/\\r\\n/g, "\\n"); +const lockWithoutLocalPackage = normalizedUpdatedLock.replace(localPackagePattern, ""); +if (lockWithoutLocalPackage === normalizedUpdatedLock) throw new Error("Cargo did not register the local Kascov preflight package"); +if (lockWithoutLocalPackage !== normalizedPinnedLock) throw new Error("Pinned Kascov dependency lockfile drifted while adding the local preflight package"); run("cargo", [ "build", "--locked", From b2e4b953444845defea6c120ee3dac61ab25273e Mon Sep 17 00:00:00 2001 From: w00c00 <46839097+w00c00@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:54:18 +0800 Subject: [PATCH 4/4] Validate pinned Cargo package identities --- scripts/build-kascov-preflight.mjs | 33 +++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/scripts/build-kascov-preflight.mjs b/scripts/build-kascov-preflight.mjs index 4b68d1a..d413e5f 100644 --- a/scripts/build-kascov-preflight.mjs +++ b/scripts/build-kascov-preflight.mjs @@ -17,6 +17,17 @@ function run(command, args, options = {}) { execFileSync(command, args, { stdio: "inherit", ...options }); } +function packageIdentities(lock) { + return lock + .replace(/\r\n/g, "\n") + .split(/\n(?=\[\[package\]\]\n)/) + .filter((section) => section.startsWith("[[package]]\n")) + .map((section) => { + const field = (name) => section.match(new RegExp(`^${name} = "([^"]*)"$`, "m"))?.[1] || ""; + return JSON.stringify([field("name"), field("version"), field("source"), field("checksum")]); + }); +} + if (!fs.existsSync(path.join(work, ".git"))) { fs.rmSync(work, { recursive: true, force: true }); fs.mkdirSync(path.dirname(work), { recursive: true }); @@ -65,18 +76,20 @@ const buildArgs = [ "--bin", "kascov-preflight" ]; // Cargo must first register the injected local package in the upstream lockfile. -// The existing lockfile supplies every external resolution. We then remove only -// that local package stanza and require the rest to match the pinned file byte -// for byte before repeating the build under --locked. +// The existing lockfile supplies every external resolution. Target-specific +// dependency sections can be rewritten on Windows, so compare immutable package +// identities instead of formatting: every pinned name/version/source/checksum +// tuple must remain present before repeating the build under --locked. run("cargo", buildArgs, { env: { ...process.env, CARGO_TARGET_DIR: targetDirectory } }); const updatedLock = fs.readFileSync(lockFile, "utf8"); -const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -const localPackagePattern = new RegExp(`\\n\\[\\[package\\]\\]\\nname = "${escapedPackageName}"\\n[\\s\\S]*?(?=\\n\\[\\[package\\]\\]|\\s*$)`); -const normalizedUpdatedLock = updatedLock.replace(/\\r\\n/g, "\\n"); -const normalizedPinnedLock = pinnedLock.replace(/\\r\\n/g, "\\n"); -const lockWithoutLocalPackage = normalizedUpdatedLock.replace(localPackagePattern, ""); -if (lockWithoutLocalPackage === normalizedUpdatedLock) throw new Error("Cargo did not register the local Kascov preflight package"); -if (lockWithoutLocalPackage !== normalizedPinnedLock) throw new Error("Pinned Kascov dependency lockfile drifted while adding the local preflight package"); +const pinnedPackages = packageIdentities(pinnedLock); +const updatedPackages = new Set(packageIdentities(updatedLock)); +for (const identity of pinnedPackages) { + if (!updatedPackages.has(identity)) throw new Error(`Pinned Kascov dependency drifted while adding the local preflight package: ${identity}`); +} +if (![...updatedPackages].some((identity) => JSON.parse(identity)[0] === packageName)) { + throw new Error("Cargo did not register the local Kascov preflight package"); +} run("cargo", [ "build", "--locked",