diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24d406c..91a3a65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,6 +109,15 @@ jobs: done ls -la release-assets/ + - name: Stage binaries for npm package + run: | + install -Dm755 release-assets/gale-aarch64-apple-darwin npm/bin/aarch64-apple-darwin/gale + install -Dm755 release-assets/gale-x86_64-apple-darwin npm/bin/x86_64-apple-darwin/gale + install -Dm755 release-assets/gale-aarch64-unknown-linux-gnu npm/bin/aarch64-unknown-linux-gnu/gale + install -Dm755 release-assets/gale-x86_64-unknown-linux-gnu npm/bin/x86_64-unknown-linux-gnu/gale + chmod +x npm/bin/gale + find npm/bin -maxdepth 3 -type f -print + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: diff --git a/PUBLISHING.md b/PUBLISHING.md index ce616d6..8b79d17 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -1,28 +1,29 @@ # Publishing Gale to npm -Gale is distributed on npm as `@lyricalstring/gale`. The package uses a postinstall -script that downloads the correct precompiled binary from GitHub Releases. +Gale is distributed on npm as `@lyricalstring/gale`. The package includes +precompiled binaries for supported platforms in the npm tarball, so install does +not run a lifecycle script or download executables from GitHub Releases. ## Package layout ``` npm/ package.json @lyricalstring/gale — main package - install.js Postinstall script (downloads platform binary from GitHub Releases) - bin/gale Placeholder script (replaced by real binary on install) + bin/gale POSIX launcher that selects the current platform binary + bin// Precompiled platform binaries README.md npm page README ``` -When a user runs `npm install @lyricalstring/gale`, the postinstall script -(`install.js`) detects the platform and downloads the matching binary from the -GitHub Release matching the package version. +When a user runs `npm install @lyricalstring/gale`, package managers unpack the +launcher and the platform binaries directly from the npm tarball. Running +`gale` executes `bin/gale`, which selects the matching native binary. Supported platforms: `darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`. ## Prerequisites - npm account with publish access to the `@lyricalstring` scope -- GitHub Release with precompiled binaries for the target version +- Precompiled binaries for every supported platform ## Release process @@ -39,14 +40,16 @@ git push && git push --tags The workflow will: 1. Build Linux binaries (x64 + arm64) -2. Create a GitHub Release with the binaries -3. Set the npm package version to match -4. Publish `@lyricalstring/gale` to npm +2. Build macOS binaries (x64 + arm64) +3. Create a GitHub Release with the binaries +4. Stage the binaries into `npm/bin//gale` +5. Set the npm package version to match +6. Publish `@lyricalstring/gale` to npm ### Option B: Manual ```bash -# 1. Build the binary for your platform +# 1. Build and stage the binary for your platform ./scripts/build-npm.sh # 2. Set the version @@ -56,9 +59,8 @@ The workflow will: cd npm && npm publish --access public ``` -Note: For users to install successfully, the GitHub Release for the matching -version must contain binaries for all supported platforms. Use the CI release -workflow for production releases. +Note: Production npm releases must include binaries for all supported platforms. +Use the CI release workflow for production releases. ## Rust targets reference @@ -81,7 +83,7 @@ cargo install gale-lint ## Testing locally ```bash -# 1. Build for your current platform +# 1. Build and stage for your current platform ./scripts/build-npm.sh # 2. Pack (dry run) to see what would be published @@ -96,9 +98,9 @@ npx gale --version ## Troubleshooting -**"Gale binary not found"**: The postinstall download may have failed. Run -`npm rebuild @lyricalstring/gale` to retry, or check network access to -`github.com`. +**"Gale binary missing"**: The npm tarball did not include the expected +`bin//gale` file. Reinstall `@lyricalstring/gale`; if the error +persists, report a packaging bug. -**Unsupported platform**: The postinstall script only supports darwin and linux -on arm64/x64. Windows is not yet supported via npm. Build from source instead. +**Unsupported platform**: The npm launcher supports darwin and linux on +arm64/x64. Windows is not yet supported via npm. Build from source instead. diff --git a/README.md b/README.md index b93b3de..2a17f96 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,9 @@ Your `.stylelintrc` stays exactly the same. Gale reads the same config files, fo npm install -D @lyricalstring/gale ``` -The npm package automatically downloads the correct platform binary on install. Supported platforms: macOS (arm64, x64), Linux (x64, arm64). +The npm package includes prebuilt binaries for supported platforms, so install +does not run a postinstall script or download executables. Supported platforms: +macOS (arm64, x64), Linux (x64, arm64). ### Cargo @@ -327,15 +329,16 @@ The [release workflow](.github/workflows/release.yml) will: 1. Build binaries for Linux (x64, arm64) and macOS (x64, arm64) 2. Create a GitHub Release with the binaries -3. Publish the npm package (`@lyricalstring/gale`) with the matching version +3. Stage those binaries inside the npm package +4. Publish the npm package (`@lyricalstring/gale`) with the matching version ### Manual npm build ```bash -# Build for current platform +# Build and stage the current platform binary in npm/bin//gale ./scripts/build-npm.sh -# Build for all platforms (requires cross + Docker) +# Build and stage all supported binaries (requires cross + Docker) ./scripts/build-npm.sh --all # Set npm package version before building diff --git a/npm/bin/gale b/npm/bin/gale old mode 100644 new mode 100755 index 52a158f..a142302 --- a/npm/bin/gale +++ b/npm/bin/gale @@ -1,3 +1,52 @@ #!/bin/sh -echo "Gale binary not found. Run 'npm rebuild @lyricalstring/gale' to install." -exit 1 + +set -eu + +platform="$(uname -s)-$(uname -m)" + +case "$platform" in + Darwin-arm64) + target="aarch64-apple-darwin" + ;; + Darwin-x86_64) + target="x86_64-apple-darwin" + ;; + Linux-aarch64|Linux-arm64) + target="aarch64-unknown-linux-gnu" + ;; + Linux-x86_64) + target="x86_64-unknown-linux-gnu" + ;; + *) + echo "Unsupported platform: $platform" >&2 + echo "Supported: Darwin-arm64, Darwin-x86_64, Linux-aarch64, Linux-x86_64" >&2 + exit 1 + ;; +esac + +script="$0" + +while [ -L "$script" ]; do + script_dir="$(CDPATH= cd "$(dirname "$script")" && pwd)" + link="$(readlink "$script")" + + case "$link" in + /*) + script="$link" + ;; + *) + script="$script_dir/$link" + ;; + esac +done + +bin_dir="$(CDPATH= cd "$(dirname "$script")" && pwd)" +binary="$bin_dir/$target/gale" + +if [ ! -x "$binary" ]; then + echo "Gale binary missing for $target at $binary" >&2 + echo "This package should include prebuilt binaries. Please reinstall @lyricalstring/gale or report a packaging bug." >&2 + exit 1 +fi + +exec "$binary" "$@" diff --git a/npm/index.cjs b/npm/index.cjs index aa4ff53..ffbc848 100644 --- a/npm/index.cjs +++ b/npm/index.cjs @@ -1,3 +1,3 @@ // This file exists so that tools like `resolve-bin` and `require.resolve()` -// can locate this package. The actual linter is the native binary in bin/. +// can locate this package. The actual linter is launched by bin/gale. module.exports = {}; diff --git a/npm/install.cjs b/npm/install.cjs deleted file mode 100644 index 240e7c5..0000000 --- a/npm/install.cjs +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env node - -const fs = require("fs"); -const path = require("path"); -const https = require("https"); - -const REPO = "LyricalString/gale"; -const VERSION = require("./package.json").version; - -const PLATFORM_MAP = { - "darwin-arm64": "gale-aarch64-apple-darwin", - "darwin-x64": "gale-x86_64-apple-darwin", - "linux-arm64": "gale-aarch64-unknown-linux-gnu", - "linux-x64": "gale-x86_64-unknown-linux-gnu", -}; - -function getPlatformKey() { - return `${process.platform}-${process.arch}`; -} - -function getBinaryName() { - const key = getPlatformKey(); - const name = PLATFORM_MAP[key]; - if (!name) { - console.error(`Unsupported platform: ${key}`); - console.error(`Supported: ${Object.keys(PLATFORM_MAP).join(", ")}`); - process.exit(1); - } - return name; -} - -function download(url) { - return new Promise((resolve, reject) => { - const follow = (url, redirects = 0) => { - if (redirects > 5) return reject(new Error("Too many redirects")); - - https.get(url, { headers: { "User-Agent": "gale-npm" } }, (res) => { - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - return follow(res.headers.location, redirects + 1); - } - if (res.statusCode !== 200) { - return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`)); - } - const chunks = []; - res.on("data", (chunk) => chunks.push(chunk)); - res.on("end", () => resolve(Buffer.concat(chunks))); - res.on("error", reject); - }).on("error", reject); - }; - follow(url); - }); -} - -async function main() { - const binaryName = getBinaryName(); - const binDir = path.join(__dirname, "bin"); - const binPath = path.join(binDir, "gale"); - - // Skip if already installed (real binary, not placeholder) - if (fs.existsSync(binPath)) { - const stat = fs.statSync(binPath); - if (stat.size > 1024) { - return; - } - } - - const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${binaryName}`; - console.log(`Downloading Gale v${VERSION} for ${getPlatformKey()}...`); - - try { - const data = await download(url); - fs.mkdirSync(binDir, { recursive: true }); - fs.writeFileSync(binPath, data); - fs.chmodSync(binPath, 0o755); - console.log("Gale installed successfully."); - } catch (err) { - console.error(`Failed to download Gale: ${err.message}`); - console.error(`URL: ${url}`); - console.error(`You can install manually from: https://github.com/${REPO}/releases`); - process.exit(1); - } -} - -main(); diff --git a/npm/package.json b/npm/package.json index 990d155..9e9be32 100644 --- a/npm/package.json +++ b/npm/package.json @@ -31,14 +31,10 @@ "bin": { "gale": "bin/gale" }, - "scripts": { - "postinstall": "node install.cjs" - }, "files": [ "bin/", "index.cjs", "index.mjs", - "install.cjs", "README.md" ], "engines": { diff --git a/scripts/build-npm.sh b/scripts/build-npm.sh index 469f000..7e6a651 100755 --- a/scripts/build-npm.sh +++ b/scripts/build-npm.sh @@ -47,13 +47,13 @@ if [[ -n "$VERSION" ]]; then fi # -------------------------------------------------------------------------- -# Target definitions: rust_target -> artifact_name +# Supported Rust targets # -------------------------------------------------------------------------- -declare -A TARGETS=( - ["aarch64-apple-darwin"]="gale-aarch64-apple-darwin" - ["x86_64-apple-darwin"]="gale-x86_64-apple-darwin" - ["x86_64-unknown-linux-gnu"]="gale-x86_64-unknown-linux-gnu" - ["aarch64-unknown-linux-gnu"]="gale-aarch64-unknown-linux-gnu" +TARGETS=( + "aarch64-apple-darwin" + "x86_64-apple-darwin" + "x86_64-unknown-linux-gnu" + "aarch64-unknown-linux-gnu" ) # -------------------------------------------------------------------------- @@ -93,9 +93,9 @@ build_target() { local src="$ROOT/target/release/gale" fi - local dest="$NPM_DIR/bin/gale" + local dest="$NPM_DIR/bin/$rust_target/gale" echo " Copying $src -> $dest" - mkdir -p "$NPM_DIR/bin" + mkdir -p "$(dirname "$dest")" cp "$src" "$dest" chmod +x "$dest" echo " Done: $rust_target" @@ -119,7 +119,7 @@ if [[ "$BUILD_ALL" == "true" ]]; then CURRENT_TARGET="$(detect_current_target)" - for rust_target in "${!TARGETS[@]}"; do + for rust_target in "${TARGETS[@]}"; do if [[ "$rust_target" == "$CURRENT_TARGET" ]]; then build_target "$rust_target" "false" else @@ -137,7 +137,7 @@ echo "==========================================" echo " Build complete!" echo "==========================================" echo "" -echo "Binary ready in: $NPM_DIR/bin/gale" +echo "Binaries ready in: $NPM_DIR/bin/" echo "" echo "To publish:" echo ""