From f76d4266edfac5cfb42a73af98e510076e86ac05 Mon Sep 17 00:00:00 2001 From: kite Date: Wed, 17 Jun 2026 12:44:00 +0800 Subject: [PATCH] feat: add platform-specific npm packages to eliminate postinstall download Ship Go binaries inside per-platform npm packages (@alibaba-group/ocr-{os}-{arch}) so npm install resolves the correct binary via optionalDependencies + os/cpu fields. This removes the need for a postinstall download from GitHub Releases, which is extremely slow for users behind restricted networks (e.g. China mainland). The postinstall download is retained as a fallback for --no-optional installs. --- .github/workflows/release.yml | 46 ++++++++++++- .gitignore | 1 + .npmignore | 1 + bin/ocr.js | 12 +++- npm/darwin-arm64/package.json | 23 +++++++ npm/darwin-x64/package.json | 23 +++++++ npm/linux-arm64/package.json | 23 +++++++ npm/linux-x64/package.json | 23 +++++++ npm/win32-arm64/package.json | 23 +++++++ npm/win32-x64/package.json | 23 +++++++ package.json | 8 +++ scripts/install.js | 10 ++- scripts/platform.js | 52 ++++++++++++++ scripts/publish/publish-platform.sh | 103 ++++++++++++++++++++++++++++ scripts/publish/publish.sh | 14 +++- scripts/update.js | 6 ++ 16 files changed, 384 insertions(+), 7 deletions(-) create mode 100644 npm/darwin-arm64/package.json create mode 100644 npm/darwin-x64/package.json create mode 100644 npm/linux-arm64/package.json create mode 100644 npm/linux-x64/package.json create mode 100644 npm/win32-arm64/package.json create mode 100644 npm/win32-x64/package.json create mode 100644 scripts/platform.js create mode 100755 scripts/publish/publish-platform.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4c8935..dc955e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,14 +93,56 @@ jobs: - name: Install jq run: apt-get update && apt-get install -y jq - - name: Inject version from tag - run: jq --arg v "${GITHUB_REF_NAME#v}" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json + - uses: actions/download-artifact@v4 + with: + pattern: binary-* + merge-multiple: true - name: Configure npm registry run: echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish platform packages + run: | + VERSION="${GITHUB_REF_NAME#v}" + PLATFORMS="darwin-arm64:opencodereview-darwin-arm64:opencodereview + darwin-x64:opencodereview-darwin-amd64:opencodereview + linux-arm64:opencodereview-linux-arm64:opencodereview + linux-x64:opencodereview-linux-amd64:opencodereview + win32-arm64:opencodereview-windows-arm64.exe:opencodereview.exe + win32-x64:opencodereview-windows-amd64.exe:opencodereview.exe" + + for entry in $PLATFORMS; do + IFS=':' read -r platform_dir dist_binary dest_name <<< "$entry" + pkg_dir="npm/$platform_dir" + mkdir -p "$pkg_dir/bin" + cp "$dist_binary" "$pkg_dir/bin/$dest_name" + chmod 755 "$pkg_dir/bin/$dest_name" 2>/dev/null || true + + jq --arg v "$VERSION" '.version = $v' "$pkg_dir/package.json" > "$pkg_dir/package.json.tmp" + mv "$pkg_dir/package.json.tmp" "$pkg_dir/package.json" + + pkg_name=$(jq -r '.name' "$pkg_dir/package.json") + already=$(npm view "${pkg_name}@${VERSION}" version 2>/dev/null || true) + if [ "$already" = "$VERSION" ]; then + echo "Skip: ${pkg_name}@${VERSION} already published" + else + npm publish "$pkg_dir" --access public + echo "Published: ${pkg_name}@${VERSION}" + fi + done + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Inject version and optionalDependencies + run: | + VERSION="${GITHUB_REF_NAME#v}" + jq --arg v "$VERSION" ' + .version = $v | + .optionalDependencies |= with_entries(.value = $v) + ' package.json > package.json.tmp && mv package.json.tmp package.json + - name: Publish to npm run: npm publish --access public env: diff --git a/.gitignore b/.gitignore index 1d144a5..eacaefb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.iws dist/ +npm/*/bin/ .claude/plans .claude/settings.local.json diff --git a/.npmignore b/.npmignore index 3c69822..520a35b 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,7 @@ cmd/ internal/ dist/ +npm/ pages/ Makefile go.mod diff --git a/bin/ocr.js b/bin/ocr.js index 6bc86bd..33064d0 100755 --- a/bin/ocr.js +++ b/bin/ocr.js @@ -6,8 +6,16 @@ const path = require("path"); const fs = require("fs"); const os = require("os"); -const IS_WINDOWS = process.platform === "win32"; -const binaryPath = path.join(__dirname, IS_WINDOWS ? "opencodereview.exe" : "opencodereview"); +const { resolveNativeBinary } = require("../scripts/platform"); + +const resolved = resolveNativeBinary(); +if (!resolved) { + console.error( + "[ERROR] OpenCodeReview binary not found. Run: npm install -g @alibaba-group/open-code-review" + ); + process.exit(1); +} +const binaryPath = resolved.path; if (!process.env.OCR_NO_UPDATE) { const stateDir = path.join(os.homedir(), ".opencodereview"); diff --git a/npm/darwin-arm64/package.json b/npm/darwin-arm64/package.json new file mode 100644 index 0000000..d5eadb5 --- /dev/null +++ b/npm/darwin-arm64/package.json @@ -0,0 +1,23 @@ +{ + "name": "@alibaba-group/ocr-darwin-arm64", + "version": "0.0.0", + "description": "Platform binary for open-code-review (darwin arm64)", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "files": [ + "bin/" + ], + "preferUnplugged": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alibaba/open-code-review.git" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/npm/darwin-x64/package.json b/npm/darwin-x64/package.json new file mode 100644 index 0000000..0e3f068 --- /dev/null +++ b/npm/darwin-x64/package.json @@ -0,0 +1,23 @@ +{ + "name": "@alibaba-group/ocr-darwin-x64", + "version": "0.0.0", + "description": "Platform binary for open-code-review (darwin x64)", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin/" + ], + "preferUnplugged": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alibaba/open-code-review.git" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/npm/linux-arm64/package.json b/npm/linux-arm64/package.json new file mode 100644 index 0000000..dc10b56 --- /dev/null +++ b/npm/linux-arm64/package.json @@ -0,0 +1,23 @@ +{ + "name": "@alibaba-group/ocr-linux-arm64", + "version": "0.0.0", + "description": "Platform binary for open-code-review (linux arm64)", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "files": [ + "bin/" + ], + "preferUnplugged": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alibaba/open-code-review.git" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/npm/linux-x64/package.json b/npm/linux-x64/package.json new file mode 100644 index 0000000..31a9a8e --- /dev/null +++ b/npm/linux-x64/package.json @@ -0,0 +1,23 @@ +{ + "name": "@alibaba-group/ocr-linux-x64", + "version": "0.0.0", + "description": "Platform binary for open-code-review (linux x64)", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin/" + ], + "preferUnplugged": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alibaba/open-code-review.git" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/npm/win32-arm64/package.json b/npm/win32-arm64/package.json new file mode 100644 index 0000000..082a7a7 --- /dev/null +++ b/npm/win32-arm64/package.json @@ -0,0 +1,23 @@ +{ + "name": "@alibaba-group/ocr-win32-arm64", + "version": "0.0.0", + "description": "Platform binary for open-code-review (win32 arm64)", + "os": [ + "win32" + ], + "cpu": [ + "arm64" + ], + "files": [ + "bin/" + ], + "preferUnplugged": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alibaba/open-code-review.git" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/npm/win32-x64/package.json b/npm/win32-x64/package.json new file mode 100644 index 0000000..aaf6588 --- /dev/null +++ b/npm/win32-x64/package.json @@ -0,0 +1,23 @@ +{ + "name": "@alibaba-group/ocr-win32-x64", + "version": "0.0.0", + "description": "Platform binary for open-code-review (win32 x64)", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin/" + ], + "preferUnplugged": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alibaba/open-code-review.git" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/package.json b/package.json index 2809c16..ce2c783 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,14 @@ "urlPattern": "https://github.com/alibaba/open-code-review/releases/download/v{version}/opencodereview-{os}-{arch}", "checksumPattern": "https://github.com/alibaba/open-code-review/releases/download/v{version}/sha256sum.txt" }, + "optionalDependencies": { + "@alibaba-group/ocr-darwin-arm64": "0.0.0", + "@alibaba-group/ocr-darwin-x64": "0.0.0", + "@alibaba-group/ocr-linux-arm64": "0.0.0", + "@alibaba-group/ocr-linux-x64": "0.0.0", + "@alibaba-group/ocr-win32-arm64": "0.0.0", + "@alibaba-group/ocr-win32-x64": "0.0.0" + }, "engines": { "node": ">=14" }, diff --git a/scripts/install.js b/scripts/install.js index 4c59b97..2b34340 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -6,8 +6,7 @@ const path = require("path"); const https = require("https"); const crypto = require("crypto"); -const IS_WINDOWS = process.platform === "win32"; -const BINARY_NAME = IS_WINDOWS ? "opencodereview.exe" : "opencodereview"; +const { IS_WINDOWS, BINARY_FILENAME: BINARY_NAME, resolveNativeBinary } = require("./platform"); const packageRoot = path.join(__dirname, ".."); const binDir = path.join(packageRoot, "bin"); @@ -154,6 +153,13 @@ async function main() { info("OpenCodeReview Installer"); info("========================="); + const existing = resolveNativeBinary(); + if (existing && existing.fromPlatformPkg) { + info("Binary provided by platform package, skipping download."); + info(` ${existing.path}`); + return; + } + const { os, arch } = detectPlatform(); info(`Detected platform: ${os}/${arch}`); diff --git a/scripts/platform.js b/scripts/platform.js new file mode 100644 index 0000000..0fafae0 --- /dev/null +++ b/scripts/platform.js @@ -0,0 +1,52 @@ +"use strict"; + +const path = require("path"); +const fs = require("fs"); + +const IS_WINDOWS = process.platform === "win32"; +const BINARY_FILENAME = IS_WINDOWS ? "opencodereview.exe" : "opencodereview"; + +const PLATFORM_PKG = { + "darwin-arm64": "@alibaba-group/ocr-darwin-arm64", + "darwin-x64": "@alibaba-group/ocr-darwin-x64", + "linux-arm64": "@alibaba-group/ocr-linux-arm64", + "linux-x64": "@alibaba-group/ocr-linux-x64", + "win32-arm64": "@alibaba-group/ocr-win32-arm64", + "win32-x64": "@alibaba-group/ocr-win32-x64", +}; + +function getPlatformPackageName() { + return PLATFORM_PKG[`${process.platform}-${process.arch}`] || null; +} + +function resolveNativeBinary() { + const pkgName = getPlatformPackageName(); + if (pkgName) { + try { + const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`)); + const binPath = path.join(pkgDir, "bin", BINARY_FILENAME); + if (fs.existsSync(binPath)) { + return { path: binPath, fromPlatformPkg: true }; + } + } catch (err) { + if (err.code !== "MODULE_NOT_FOUND") { + console.warn(`[WARN] Unexpected error resolving ${pkgName}: ${err.message}`); + } + } + } + + const legacyPath = path.join(__dirname, "..", "bin", BINARY_FILENAME); + if (fs.existsSync(legacyPath)) { + return { path: legacyPath, fromPlatformPkg: false }; + } + + return null; +} + +module.exports = { + IS_WINDOWS, + BINARY_FILENAME, + PLATFORM_PKG, + getPlatformPackageName, + resolveNativeBinary, +}; diff --git a/scripts/publish/publish-platform.sh b/scripts/publish/publish-platform.sh new file mode 100755 index 0000000..4a3cd15 --- /dev/null +++ b/scripts/publish/publish-platform.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# +# publish-platform.sh — Publish platform-specific npm packages. +# +# Expects NPM_VERSION to be exported by the caller (publish.sh). +# Each platform package ships the Go binary for a single OS/arch combination. +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/_common.sh" + +PROJECT_ROOT="$(resolve_project_root)" + +[ -z "${NPM_VERSION:-}" ] && die "NPM_VERSION is not set. This script should be called from publish.sh." + +# Go dist/ binary name → npm platform directory +# Format: "npm_dir:dist_binary:dest_filename" +PLATFORMS=( + "darwin-arm64:opencodereview-darwin-arm64:opencodereview" + "darwin-x64:opencodereview-darwin-amd64:opencodereview" + "linux-arm64:opencodereview-linux-arm64:opencodereview" + "linux-x64:opencodereview-linux-amd64:opencodereview" + "win32-arm64:opencodereview-windows-arm64.exe:opencodereview.exe" + "win32-x64:opencodereview-windows-amd64.exe:opencodereview.exe" +) + +REGISTRY_ARGS=() +if [ -n "${OCR_PUBLISH_REGISTRY:-}" ]; then + REGISTRY_ARGS=(--registry "$OCR_PUBLISH_REGISTRY") +fi + +# Pre-check all binaries exist before publishing anything +for entry in "${PLATFORMS[@]}"; do + IFS=':' read -r _ dist_binary _ <<< "$entry" + [ -f "$PROJECT_ROOT/dist/$dist_binary" ] || die "Binary not found: $PROJECT_ROOT/dist/$dist_binary" +done + +FAILED_PLATFORMS=() +CURRENT_BACKUP="" +CURRENT_PKG_JSON="" +CURRENT_BIN_DIR="" + +cleanup_current() { + if [ -n "$CURRENT_BACKUP" ] && [ -n "$CURRENT_PKG_JSON" ]; then + cp "$CURRENT_BACKUP" "$CURRENT_PKG_JSON" 2>/dev/null || true + rm -f "$CURRENT_BACKUP" + fi + if [ -n "$CURRENT_BIN_DIR" ]; then + rm -rf "$CURRENT_BIN_DIR" + fi + CURRENT_BACKUP="" + CURRENT_PKG_JSON="" + CURRENT_BIN_DIR="" +} + +trap cleanup_current EXIT + +for entry in "${PLATFORMS[@]}"; do + IFS=':' read -r platform_dir dist_binary dest_name <<< "$entry" + + pkg_dir="$PROJECT_ROOT/npm/$platform_dir" + bin_dir="$pkg_dir/bin" + dist_path="$PROJECT_ROOT/dist/$dist_binary" + pkg_json="$pkg_dir/package.json" + + info "Preparing $platform_dir..." + mkdir -p "$bin_dir" + cp "$dist_path" "$bin_dir/$dest_name" + chmod 755 "$bin_dir/$dest_name" 2>/dev/null || true + + CURRENT_BACKUP=$(mktemp) + CURRENT_PKG_JSON="$pkg_json" + CURRENT_BIN_DIR="$bin_dir" + cp "$pkg_json" "$CURRENT_BACKUP" + + jq --arg v "$NPM_VERSION" '.version = $v' "$pkg_json" > "${pkg_json}.tmp" && mv "${pkg_json}.tmp" "$pkg_json" || { rm -f "${pkg_json}.tmp"; false; } + + pkg_name=$(jq -r '.name' "$pkg_json") + already=$(npm view "${pkg_name}@${NPM_VERSION}" version ${REGISTRY_ARGS[@]+"${REGISTRY_ARGS[@]}"} 2>/dev/null || true) + if [ "$already" = "$NPM_VERSION" ]; then + warn " ${pkg_name}@${NPM_VERSION} already published, skipping." + else + info " Publishing ${pkg_name}@${NPM_VERSION}..." + if ! npm publish "$pkg_dir" --access public ${REGISTRY_ARGS[@]+"${REGISTRY_ARGS[@]}"}; then + warn " Failed to publish ${pkg_name}@${NPM_VERSION}" + FAILED_PLATFORMS+=("$platform_dir") + else + success " Published ${pkg_name}@${NPM_VERSION}" + fi + fi + + cleanup_current +done + +trap - EXIT + +if [ ${#FAILED_PLATFORMS[@]} -gt 0 ]; then + die "Failed to publish platforms: ${FAILED_PLATFORMS[*]}" +fi + +success "All platform packages processed." diff --git a/scripts/publish/publish.sh b/scripts/publish/publish.sh index 5e34aa7..100274a 100755 --- a/scripts/publish/publish.sh +++ b/scripts/publish/publish.sh @@ -100,7 +100,13 @@ else "make -C \"$PROJECT_ROOT\" dist" fi -# ── (version is injected temporarily during patch_package_json below) ──────── +# ── Publish platform-specific packages ──────────────────────────────────────── +if [ "$SKIP_PUBLISH" = "1" ]; then + warn "Skipping platform packages (OCR_SKIP_PUBLISH=1)" +else + run_step "Publishing platform packages" \ + "bash \"$SCRIPT_DIR/publish-platform.sh\"" +fi # ── Upload to git-based release repo (optional) ────────────────────────────── upload_to_git_repo() { @@ -188,6 +194,12 @@ patch_package_json() { jq --arg v "$NPM_VERSION" '.version = $v' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp" info " version → ${NPM_VERSION}" + # Pin optionalDependencies (platform packages) to the same version + if jq -e '.optionalDependencies' "$tmp" >/dev/null 2>&1; then + jq --arg v "$NPM_VERSION" '.optionalDependencies |= with_entries(.value = $v)' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp" + info " optionalDependencies → all pinned to ${NPM_VERSION}" + fi + if [ -n "${OCR_PKG_NAME:-}" ]; then jq --arg n "$OCR_PKG_NAME" '.name = $n' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp" info " name → ${OCR_PKG_NAME}" diff --git a/scripts/update.js b/scripts/update.js index 8cb407a..a955d2d 100644 --- a/scripts/update.js +++ b/scripts/update.js @@ -147,6 +147,12 @@ async function main() { cleanupTemp(); try { + const { resolveNativeBinary } = require("./platform"); + const resolved = resolveNativeBinary(); + if (resolved && resolved.fromPlatformPkg) { + info("Binary managed by platform package, skipping auto-update."); + return; + } const installedVersion = getInstalledVersion(); if (!installedVersion) return;