From d99d8c181ac5f6f22e6a4bbc90bfd7eef42bc440 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Mon, 27 Apr 2026 10:11:19 +0100 Subject: [PATCH 1/2] feat(npm): distribute the CLI via npm Adds the build and publish tooling for distributing the Upsun CLI as an npm package, so users can run `npm install -g upsun` or `npx upsun`. Implements the optionalDependencies pattern used by esbuild, swc, biome, turbo, and similar tools. A thin wrapper package (`upsun`) declares four platform-specific packages as optionalDependencies; npm uses each package's `os` and `cpu` fields to install only the matching one. The wrapper's bin script resolves that package at runtime and execs the embedded binary, forwarding argv, stdio, exit code, and signals. No postinstall script and no runtime download. Packages produced per release: upsun wrapper @upsun/cli-linux-x64 @upsun/cli-linux-arm64 @upsun/cli-darwin universal binary, x64 and arm64 @upsun/cli-win32-x64 macOS uses the universal binary that GoReleaser already builds, so a single darwin package covers both Apple Silicon and Intel. publish.sh classifies each tarball by reading its package.json once (wrapper = "upsun", everything else is a platform package), publishes the platform packages, waits for them to become queryable on the public registry, then publishes the wrapper. The wait matters: npm publish returns success before the package is visible to npm view, and any user running npx in that window gets a broken install in their npx cache that does not self-heal. Layout: - npm/wrapper: shim package with bin/upsun.js - npm/platform-template: template for the per-platform packages - npm/scripts/build.sh: assembles tarballs from GoReleaser archives - npm/scripts/publish.sh: classifies, publishes, waits, then wrapper - Makefile targets: npm-pack, npm-publish, npm-clean Verified end-to-end against the live registry: build, publish, install via `npx upsun`, exec, argv passthrough, and exit-code propagation all work. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + Makefile | 15 +++ npm/README.md | 78 ++++++++++++ npm/platform-template/README.md.tmpl | 7 ++ npm/platform-template/package.json.tmpl | 17 +++ npm/scripts/build.sh | 151 ++++++++++++++++++++++++ npm/scripts/publish.sh | 95 +++++++++++++++ npm/wrapper/README.md | 21 ++++ npm/wrapper/bin/upsun.js | 53 +++++++++ npm/wrapper/package.json.tmpl | 27 +++++ 10 files changed, 465 insertions(+) create mode 100644 npm/README.md create mode 100644 npm/platform-template/README.md.tmpl create mode 100644 npm/platform-template/package.json.tmpl create mode 100755 npm/scripts/build.sh create mode 100755 npm/scripts/publish.sh create mode 100644 npm/wrapper/README.md create mode 100644 npm/wrapper/bin/upsun.js create mode 100644 npm/wrapper/package.json.tmpl diff --git a/.gitignore b/.gitignore index ce600154a..1ec02cca2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ internal/legacy/archives/* dist/ +npm/dist/ php-* completion diff --git a/Makefile b/Makefile index ca53b8599..01f206c2f 100644 --- a/Makefile +++ b/Makefile @@ -155,3 +155,18 @@ vendor-snapshot: check-vendor .goreleaser.vendor.yaml goreleaser internal/legacy .PHONY: goreleaser-check goreleaser-check: goreleaser ## Check the goreleaser configs PHP_VERSION=$(PHP_VERSION) goreleaser check --config=.goreleaser.yaml + +# ----- npm distribution ----- +# See npm/README.md. + +.PHONY: npm-pack +npm-pack: ## Build npm tarballs from existing GoReleaser archives in dist/ + bash npm/scripts/build.sh + +.PHONY: npm-publish +npm-publish: ## Publish npm tarballs (requires npm auth). NPM_TAG=latest|next, DRY_RUN=1 to dry-run + bash npm/scripts/publish.sh + +.PHONY: npm-clean +npm-clean: ## Remove npm/dist working directory + rm -rf npm/dist diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 000000000..6dfaba3a5 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,78 @@ +# npm distribution + +Tooling to ship the Upsun CLI as an npm package, so users can run +`npm install -g upsun` or `npx upsun`. Implements the +`optionalDependencies` pattern used by esbuild, swc, biome, turbo, and +others: a small wrapper package selects the right platform-specific +package at install time, so each user only downloads the binary that +matches their OS and CPU. No postinstall script, no runtime download. + +## Packages + +| Package | Contents | +| ------------------------ | --------------------------------------- | +| `upsun` | wrapper, with the four platforms below as `optionalDependencies` | +| `@upsun/cli-linux-x64` | Linux amd64 binary | +| `@upsun/cli-linux-arm64` | Linux arm64 binary | +| `@upsun/cli-darwin` | macOS universal binary (x64 + arm64) | +| `@upsun/cli-win32-x64` | Windows amd64 binary | + +## Layout + +``` +npm/ +├── wrapper/ wrapper package source +│ ├── bin/upsun.js shim that resolves the platform package and execs the binary +│ ├── package.json.tmpl stamped with version at build time +│ └── README.md shipped to the registry as the wrapper README +├── platform-template/ common template for all platform-specific packages +│ ├── package.json.tmpl stamped per-target with name, version, os, cpu +│ └── README.md.tmpl +├── scripts/ +│ ├── build.sh assembles tarballs from GoReleaser archives +│ └── publish.sh publishes tarballs in lockstep +└── dist/ build output (npm pack tarballs); gitignored +``` + +## Build + +```sh +make snapshot-no-nfpm # or any goreleaser invocation that writes upsun_*.tar.gz/zip into dist/ +make npm-pack # reads dist/, writes npm/dist/*.tgz +``` + +The build script resolves the version from the GoReleaser archive +filenames. Override with `VERSION=...` if you need to. + +## Publish + +```sh +make npm-publish # publish all five packages in lockstep +DRY_RUN=1 make npm-publish # validate without publishing +NPM_TAG=next make npm-publish # for prereleases +``` + +The script publishes platform packages first, then the wrapper, so the +registry is never in a state where the wrapper points at platform +packages that don't yet exist. + +Auth is via the standard npm mechanism: `~/.npmrc` with a token, or the +`actions/setup-node` action in CI populating one for you from +`NODE_AUTH_TOKEN`. The `--access public` flag is set so first-time +publishes of scoped packages do not get marked private. + +## Versioning + +Every npm release uses the same version as the corresponding GitHub +release tag. Platform packages and the wrapper are always published in +lockstep at the same version; the wrapper's `optionalDependencies` pin +exact versions, so a mismatched set will not resolve. + +## Known limitations + +- `npm install --no-optional` (or `--omit=optional`) skips the platform + package, and the wrapper exits with a clear error pointing at the flag. +- `darwin-arm64` and `darwin-x64` share a single universal binary + package. This roughly doubles the macOS install size relative to + per-arch packages, but matches the artifact GoReleaser produces and + keeps the package set smaller. diff --git a/npm/platform-template/README.md.tmpl b/npm/platform-template/README.md.tmpl new file mode 100644 index 000000000..10d45d6a4 --- /dev/null +++ b/npm/platform-template/README.md.tmpl @@ -0,0 +1,7 @@ +# __PKG_NAME__ + +Platform-specific binary for the [Upsun CLI](https://github.com/upsun/cli). + +This package is installed automatically by the `upsun` wrapper as an +`optionalDependency` matching your operating system and CPU. You do not +need to install it directly. diff --git a/npm/platform-template/package.json.tmpl b/npm/platform-template/package.json.tmpl new file mode 100644 index 000000000..4e96717bc --- /dev/null +++ b/npm/platform-template/package.json.tmpl @@ -0,0 +1,17 @@ +{ + "name": "__PKG_NAME__", + "version": "__VERSION__", + "description": "__DESCRIPTION__", + "homepage": "https://docs.upsun.com/anchors/cli/", + "repository": { + "type": "git", + "url": "git+https://github.com/upsun/cli.git" + }, + "license": "MIT", + "files": [ + "bin", + "README.md" + ], + "os": __OS__, + "cpu": __CPU__ +} diff --git a/npm/scripts/build.sh b/npm/scripts/build.sh new file mode 100755 index 000000000..29ee73434 --- /dev/null +++ b/npm/scripts/build.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# Assembles npm packages from GoReleaser archives. +# +# Inputs (env vars, all optional): +# DIST_DIR Directory containing GoReleaser archives. Default: /dist +# VERSION Package version. Default: derived from the first matching archive name. +# OUT_DIR Where to write per-package working dirs and tarballs. Default: npm/dist +# +# Produces: +# upsun (wrapper, with the four platforms below as optionalDependencies) +# @upsun/cli-linux-x64 +# @upsun/cli-linux-arm64 +# @upsun/cli-darwin (universal binary; covers x64 and arm64) +# @upsun/cli-win32-x64 + +set -euo pipefail + +NPM_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_ROOT="$(cd "${NPM_DIR}/.." && pwd)" + +DIST_DIR="${DIST_DIR:-${REPO_ROOT}/dist}" +OUT_DIR="${OUT_DIR:-${NPM_DIR}/dist}" + +if [ ! -d "${DIST_DIR}" ]; then + echo "build.sh: DIST_DIR not found: ${DIST_DIR}" >&2 + echo "Run 'goreleaser release --snapshot --clean' first, or point DIST_DIR at the archives." >&2 + exit 1 +fi + +# Maps suffix -> archive glob, binary filename, os JSON, cpu JSON. +# The darwin entry has a permissive cpu list because macOS ships a +# single universal binary that runs on both Apple Silicon and Intel. +declare -A ARCHIVE_GLOB=( + [linux-x64]="upsun_*_linux_amd64.tar.gz" + [linux-arm64]="upsun_*_linux_arm64.tar.gz" + [darwin]="upsun_*_darwin_all.tar.gz" + [win32-x64]="upsun_*_windows_amd64.zip" +) +declare -A BIN_NAME=( + [linux-x64]="upsun" + [linux-arm64]="upsun" + [darwin]="upsun" + [win32-x64]="upsun.exe" +) +declare -A OS_JSON=( + [linux-x64]='["linux"]' + [linux-arm64]='["linux"]' + [darwin]='["darwin"]' + [win32-x64]='["win32"]' +) +declare -A CPU_JSON=( + [linux-x64]='["x64"]' + [linux-arm64]='["arm64"]' + [darwin]='["x64","arm64"]' + [win32-x64]='["x64"]' +) +declare -A DESCRIPTION=( + [linux-x64]="Upsun CLI binary for Linux x64" + [linux-arm64]="Upsun CLI binary for Linux arm64" + [darwin]="Upsun CLI binary for macOS (universal)" + [win32-x64]="Upsun CLI binary for Windows x64" +) + +PLATFORMS=(linux-x64 linux-arm64 darwin win32-x64) + +if [ -z "${VERSION:-}" ]; then + shopt -s nullglob + matches=("${DIST_DIR}"/upsun_*_linux_amd64.tar.gz) + shopt -u nullglob + if [ ${#matches[@]} -eq 0 ]; then + echo "build.sh: no upsun_*_linux_amd64.tar.gz in ${DIST_DIR}; set VERSION explicitly" >&2 + exit 1 + fi + base="$(basename "${matches[0]}")" + # upsun_X.Y.Z_linux_amd64.tar.gz -> X.Y.Z + VERSION="${base#upsun_}" + VERSION="${VERSION%_linux_amd64.tar.gz}" +fi + +echo "build.sh: VERSION=${VERSION}" + +rm -rf "${OUT_DIR}" +mkdir -p "${OUT_DIR}" + +build_platform_pkg() { + local suffix="$1" + local glob="${ARCHIVE_GLOB[$suffix]}" + local bin="${BIN_NAME[$suffix]}" + local name="@upsun/cli-${suffix}" + + shopt -s nullglob + # shellcheck disable=SC2206 # intentional glob expansion + local archives=("${DIST_DIR}"/${glob}) + shopt -u nullglob + if [ ${#archives[@]} -eq 0 ]; then + echo "build.sh: no archive matching ${glob} in ${DIST_DIR}" >&2 + exit 1 + fi + local archive="${archives[0]}" + + local pkg_dir="${OUT_DIR}/${suffix}" + mkdir -p "${pkg_dir}/bin" + + case "${archive}" in + *.tar.gz) tar -xzf "${archive}" -C "${pkg_dir}/bin" "${bin}" ;; + *.zip) unzip -p "${archive}" "${bin}" > "${pkg_dir}/bin/${bin}" ;; + *) echo "build.sh: unsupported archive: ${archive}" >&2; exit 1 ;; + esac + chmod +x "${pkg_dir}/bin/${bin}" || true + + sed \ + -e "s|__PKG_NAME__|${name}|g" \ + -e "s|__VERSION__|${VERSION}|g" \ + -e "s|__DESCRIPTION__|${DESCRIPTION[$suffix]}|g" \ + -e "s|__OS__|${OS_JSON[$suffix]}|g" \ + -e "s|__CPU__|${CPU_JSON[$suffix]}|g" \ + "${NPM_DIR}/platform-template/package.json.tmpl" > "${pkg_dir}/package.json" + + sed -e "s|__PKG_NAME__|${name}|g" \ + "${NPM_DIR}/platform-template/README.md.tmpl" > "${pkg_dir}/README.md" + + (cd "${pkg_dir}" && npm pack --pack-destination "${OUT_DIR}" >/dev/null) + echo " packed ${name}@${VERSION}" +} + +build_wrapper_pkg() { + local pkg_dir="${OUT_DIR}/wrapper" + mkdir -p "${pkg_dir}/bin" + + sed -e "s|__VERSION__|${VERSION}|g" \ + "${NPM_DIR}/wrapper/package.json.tmpl" > "${pkg_dir}/package.json" + + cp "${NPM_DIR}/wrapper/bin/upsun.js" "${pkg_dir}/bin/upsun.js" + chmod +x "${pkg_dir}/bin/upsun.js" + + cp "${NPM_DIR}/wrapper/README.md" "${pkg_dir}/README.md" + + (cd "${pkg_dir}" && npm pack --pack-destination "${OUT_DIR}" >/dev/null) + echo " packed upsun@${VERSION}" +} + +echo "build.sh: building platform packages" +for suffix in "${PLATFORMS[@]}"; do + build_platform_pkg "$suffix" +done + +echo "build.sh: building wrapper package" +build_wrapper_pkg + +echo "build.sh: done. Tarballs in ${OUT_DIR}:" +ls -1 "${OUT_DIR}"/*.tgz diff --git a/npm/scripts/publish.sh b/npm/scripts/publish.sh new file mode 100755 index 000000000..9f387eb7a --- /dev/null +++ b/npm/scripts/publish.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Publishes the npm tarballs produced by build.sh. +# +# Inputs (env vars): +# OUT_DIR Where build.sh wrote tarballs. Default: npm/dist +# NPM_TAG dist-tag, e.g. "latest" or "next". Default: "latest" +# DRY_RUN 1 to run npm publish --dry-run. Default: 0 +# +# Auth: requires ~/.npmrc to have a working //registry.npmjs.org/:_authToken, +# or NODE_AUTH_TOKEN set with a registry-url-configured ~/.npmrc (the +# setup-node action handles this in CI). +# +# Order: platform packages first, then wait for them to become visible +# in the public registry, then publish the wrapper. The wait matters: +# npm publish returns success before the new package is queryable via +# `npm view`. If a user runs `npx upsun` in that window, npm fails to +# resolve the wrapper's optionalDependencies, treats them as failed +# (which is silent for optional deps), and caches a broken install in +# ~/.npm/_npx that will not self-heal on retry. + +set -euo pipefail + +NPM_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="${OUT_DIR:-${NPM_DIR}/dist}" +NPM_TAG="${NPM_TAG:-latest}" +DRY_RUN="${DRY_RUN:-0}" + +if [ ! -d "${OUT_DIR}" ]; then + echo "publish.sh: OUT_DIR not found: ${OUT_DIR}. Run build.sh first." >&2 + exit 1 +fi + +shopt -s nullglob +all_tarballs=("${OUT_DIR}"/*.tgz) +shopt -u nullglob + +if [ ${#all_tarballs[@]} -eq 0 ]; then + echo "publish.sh: no tarballs in ${OUT_DIR}" >&2 + exit 1 +fi + +# Classify each tarball by reading its package.json once: the wrapper is +# the one named "upsun"; everything else is a platform package. Cache +# name and version so the propagation wait does not re-open the tarball. +declare -A NAME_OF VERSION_OF +platform_tarballs=() +wrapper_tarballs=() +for t in "${all_tarballs[@]}"; do + pkg_json=$(tar -xzOf "$t" package/package.json) + NAME_OF["$t"]=$(awk -F'"' '/"name":/ { print $4; exit }' <<<"$pkg_json") + VERSION_OF["$t"]=$(awk -F'"' '/"version":/ { print $4; exit }' <<<"$pkg_json") + if [ "${NAME_OF[$t]}" = "upsun" ]; then + wrapper_tarballs+=("$t") + else + platform_tarballs+=("$t") + fi +done + +publish_one() { + local tarball="$1" + local args=(publish "$tarball" --access public --tag "${NPM_TAG}") + if [ "${DRY_RUN}" = "1" ]; then args+=(--dry-run); fi + echo " npm ${args[*]}" + npm "${args[@]}" +} + +wait_visible() { + local pkg="$1" + local version="$2" + local deadline=$(($(date +%s) + 300)) + while ! npm view "${pkg}@${version}" version >/dev/null 2>&1; do + if [ "$(date +%s)" -gt "$deadline" ]; then + echo "publish.sh: timed out waiting for ${pkg}@${version} to propagate" >&2 + exit 1 + fi + echo " waiting for ${pkg}@${version}..." + sleep 5 + done + echo " ${pkg}@${version} visible" +} + +echo "publish.sh: publishing platform packages" +for t in "${platform_tarballs[@]}"; do publish_one "$t"; done + +if [ "${DRY_RUN}" != "1" ]; then + echo "publish.sh: waiting for platform packages to propagate" + for t in "${platform_tarballs[@]}"; do + wait_visible "${NAME_OF[$t]}" "${VERSION_OF[$t]}" + done +fi + +echo "publish.sh: publishing wrapper" +for t in "${wrapper_tarballs[@]}"; do publish_one "$t"; done + +echo "publish.sh: done" diff --git a/npm/wrapper/README.md b/npm/wrapper/README.md new file mode 100644 index 000000000..5a79c8881 --- /dev/null +++ b/npm/wrapper/README.md @@ -0,0 +1,21 @@ +# Upsun CLI + +The Upsun command-line interface, packaged for npm. + +## Install + +```sh +npm install -g upsun +# or run on demand: +npx upsun --version +``` + +This package is a thin Node.js wrapper that resolves and executes a +platform-specific binary installed via `optionalDependencies`. On install, +npm picks the matching binary for your OS and architecture; nothing is +downloaded at runtime. + +## Source + +Code, issues, and full documentation live at +[github.com/upsun/cli](https://github.com/upsun/cli). diff --git a/npm/wrapper/bin/upsun.js b/npm/wrapper/bin/upsun.js new file mode 100644 index 000000000..d6bf6aa25 --- /dev/null +++ b/npm/wrapper/bin/upsun.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node +// Resolves the platform-specific package installed via optionalDependencies, +// then execs the embedded binary, forwarding argv, stdio, and exit code. + +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); + +// macOS ships a single universal binary, so both Apple Silicon and +// Intel resolve to the same "darwin" package. +const TARGETS = { + "darwin:x64": { suffix: "darwin", binary: "upsun" }, + "darwin:arm64": { suffix: "darwin", binary: "upsun" }, + "linux:x64": { suffix: "linux-x64", binary: "upsun" }, + "linux:arm64": { suffix: "linux-arm64", binary: "upsun" }, + "win32:x64": { suffix: "win32-x64", binary: "upsun.exe" }, +}; + +const target = TARGETS[`${process.platform}:${process.arch}`]; +if (!target) { + console.error( + `upsun: no prebuilt binary for ${process.platform}-${process.arch}.`, + ); + process.exit(1); +} + +const pkgName = `@upsun/cli-${target.suffix}`; + +let binary; +try { + // require.resolve handles flat, nested, and pnpm-style installs. + const pkgJsonPath = require.resolve(`${pkgName}/package.json`); + binary = path.join(path.dirname(pkgJsonPath), "bin", target.binary); +} catch (err) { + console.error( + `upsun: platform package "${pkgName}" is not installed.\n` + + `If you installed with --no-optional or --ignore-optional, reinstall without that flag.\n` + + `Original error: ${err.message}`, + ); + process.exit(1); +} + +const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" }); + +if (result.error) { + console.error(`upsun: failed to exec ${binary}: ${result.error.message}`); + process.exit(1); +} + +if (result.signal) { + process.kill(process.pid, result.signal); +} + +process.exit(result.status ?? 1); diff --git a/npm/wrapper/package.json.tmpl b/npm/wrapper/package.json.tmpl new file mode 100644 index 000000000..1301307b5 --- /dev/null +++ b/npm/wrapper/package.json.tmpl @@ -0,0 +1,27 @@ +{ + "name": "upsun", + "version": "__VERSION__", + "description": "Upsun CLI", + "homepage": "https://docs.upsun.com/anchors/cli/", + "repository": { + "type": "git", + "url": "git+https://github.com/upsun/cli.git" + }, + "license": "MIT", + "bin": { + "upsun": "bin/upsun.js" + }, + "files": [ + "bin/upsun.js", + "README.md" + ], + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@upsun/cli-linux-x64": "__VERSION__", + "@upsun/cli-linux-arm64": "__VERSION__", + "@upsun/cli-darwin": "__VERSION__", + "@upsun/cli-win32-x64": "__VERSION__" + } +} From b397b4bd1b4ac5068d1775bc9550dee7e12ff9c6 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Mon, 27 Apr 2026 10:26:10 +0100 Subject: [PATCH 2/2] fix(npm): make scripts compatible with Bash 3.2 and stricter chmod Address review comments on PR #46: - Replace declare -A associative arrays in build.sh and publish.sh with case-statement helpers and parallel arrays, so the scripts run on macOS's default /bin/bash (3.2). - Stop swallowing chmod failures on Unix targets in build.sh; only the Windows binary, where the exec bit is meaningless, keeps || true. Co-Authored-By: Claude Opus 4.7 (1M context) --- npm/scripts/build.sh | 99 ++++++++++++++++++++++++++---------------- npm/scripts/publish.sh | 21 +++++---- 2 files changed, 74 insertions(+), 46 deletions(-) diff --git a/npm/scripts/build.sh b/npm/scripts/build.sh index 29ee73434..d1171b4ad 100755 --- a/npm/scripts/build.sh +++ b/npm/scripts/build.sh @@ -27,42 +27,59 @@ if [ ! -d "${DIST_DIR}" ]; then exit 1 fi -# Maps suffix -> archive glob, binary filename, os JSON, cpu JSON. +# Per-suffix metadata. Implemented as case statements rather than +# associative arrays so the script works on macOS's default Bash 3.2. # The darwin entry has a permissive cpu list because macOS ships a # single universal binary that runs on both Apple Silicon and Intel. -declare -A ARCHIVE_GLOB=( - [linux-x64]="upsun_*_linux_amd64.tar.gz" - [linux-arm64]="upsun_*_linux_arm64.tar.gz" - [darwin]="upsun_*_darwin_all.tar.gz" - [win32-x64]="upsun_*_windows_amd64.zip" -) -declare -A BIN_NAME=( - [linux-x64]="upsun" - [linux-arm64]="upsun" - [darwin]="upsun" - [win32-x64]="upsun.exe" -) -declare -A OS_JSON=( - [linux-x64]='["linux"]' - [linux-arm64]='["linux"]' - [darwin]='["darwin"]' - [win32-x64]='["win32"]' -) -declare -A CPU_JSON=( - [linux-x64]='["x64"]' - [linux-arm64]='["arm64"]' - [darwin]='["x64","arm64"]' - [win32-x64]='["x64"]' -) -declare -A DESCRIPTION=( - [linux-x64]="Upsun CLI binary for Linux x64" - [linux-arm64]="Upsun CLI binary for Linux arm64" - [darwin]="Upsun CLI binary for macOS (universal)" - [win32-x64]="Upsun CLI binary for Windows x64" -) - PLATFORMS=(linux-x64 linux-arm64 darwin win32-x64) +archive_glob_for() { + case "$1" in + linux-x64) echo "upsun_*_linux_amd64.tar.gz" ;; + linux-arm64) echo "upsun_*_linux_arm64.tar.gz" ;; + darwin) echo "upsun_*_darwin_all.tar.gz" ;; + win32-x64) echo "upsun_*_windows_amd64.zip" ;; + *) echo "build.sh: unsupported platform suffix: $1" >&2; exit 1 ;; + esac +} + +bin_name_for() { + case "$1" in + linux-x64|linux-arm64|darwin) echo "upsun" ;; + win32-x64) echo "upsun.exe" ;; + *) echo "build.sh: unsupported platform suffix: $1" >&2; exit 1 ;; + esac +} + +os_json_for() { + case "$1" in + linux-x64|linux-arm64) echo '["linux"]' ;; + darwin) echo '["darwin"]' ;; + win32-x64) echo '["win32"]' ;; + *) echo "build.sh: unsupported platform suffix: $1" >&2; exit 1 ;; + esac +} + +cpu_json_for() { + case "$1" in + linux-x64) echo '["x64"]' ;; + linux-arm64) echo '["arm64"]' ;; + darwin) echo '["x64","arm64"]' ;; + win32-x64) echo '["x64"]' ;; + *) echo "build.sh: unsupported platform suffix: $1" >&2; exit 1 ;; + esac +} + +description_for() { + case "$1" in + linux-x64) echo "Upsun CLI binary for Linux x64" ;; + linux-arm64) echo "Upsun CLI binary for Linux arm64" ;; + darwin) echo "Upsun CLI binary for macOS (universal)" ;; + win32-x64) echo "Upsun CLI binary for Windows x64" ;; + *) echo "build.sh: unsupported platform suffix: $1" >&2; exit 1 ;; + esac +} + if [ -z "${VERSION:-}" ]; then shopt -s nullglob matches=("${DIST_DIR}"/upsun_*_linux_amd64.tar.gz) @@ -84,8 +101,8 @@ mkdir -p "${OUT_DIR}" build_platform_pkg() { local suffix="$1" - local glob="${ARCHIVE_GLOB[$suffix]}" - local bin="${BIN_NAME[$suffix]}" + local glob; glob="$(archive_glob_for "$suffix")" + local bin; bin="$(bin_name_for "$suffix")" local name="@upsun/cli-${suffix}" shopt -s nullglob @@ -106,14 +123,20 @@ build_platform_pkg() { *.zip) unzip -p "${archive}" "${bin}" > "${pkg_dir}/bin/${bin}" ;; *) echo "build.sh: unsupported archive: ${archive}" >&2; exit 1 ;; esac - chmod +x "${pkg_dir}/bin/${bin}" || true + # The exec bit is meaningless on the Windows binary, so a chmod failure + # there is benign; on Unix targets a failure means the binary won't run. + if [ "${suffix}" = "win32-x64" ]; then + chmod +x "${pkg_dir}/bin/${bin}" || true + else + chmod +x "${pkg_dir}/bin/${bin}" + fi sed \ -e "s|__PKG_NAME__|${name}|g" \ -e "s|__VERSION__|${VERSION}|g" \ - -e "s|__DESCRIPTION__|${DESCRIPTION[$suffix]}|g" \ - -e "s|__OS__|${OS_JSON[$suffix]}|g" \ - -e "s|__CPU__|${CPU_JSON[$suffix]}|g" \ + -e "s|__DESCRIPTION__|$(description_for "$suffix")|g" \ + -e "s|__OS__|$(os_json_for "$suffix")|g" \ + -e "s|__CPU__|$(cpu_json_for "$suffix")|g" \ "${NPM_DIR}/platform-template/package.json.tmpl" > "${pkg_dir}/package.json" sed -e "s|__PKG_NAME__|${name}|g" \ diff --git a/npm/scripts/publish.sh b/npm/scripts/publish.sh index 9f387eb7a..a5e8af16b 100755 --- a/npm/scripts/publish.sh +++ b/npm/scripts/publish.sh @@ -40,19 +40,24 @@ if [ ${#all_tarballs[@]} -eq 0 ]; then fi # Classify each tarball by reading its package.json once: the wrapper is -# the one named "upsun"; everything else is a platform package. Cache -# name and version so the propagation wait does not re-open the tarball. -declare -A NAME_OF VERSION_OF +# the one named "upsun"; everything else is a platform package. Names +# and versions for platform tarballs are cached in parallel arrays so +# the propagation wait does not re-open the tarball. Parallel arrays +# rather than associative arrays so this works on macOS's default Bash 3.2. platform_tarballs=() +platform_names=() +platform_versions=() wrapper_tarballs=() for t in "${all_tarballs[@]}"; do pkg_json=$(tar -xzOf "$t" package/package.json) - NAME_OF["$t"]=$(awk -F'"' '/"name":/ { print $4; exit }' <<<"$pkg_json") - VERSION_OF["$t"]=$(awk -F'"' '/"version":/ { print $4; exit }' <<<"$pkg_json") - if [ "${NAME_OF[$t]}" = "upsun" ]; then + name=$(awk -F'"' '/"name":/ { print $4; exit }' <<<"$pkg_json") + version=$(awk -F'"' '/"version":/ { print $4; exit }' <<<"$pkg_json") + if [ "$name" = "upsun" ]; then wrapper_tarballs+=("$t") else platform_tarballs+=("$t") + platform_names+=("$name") + platform_versions+=("$version") fi done @@ -84,8 +89,8 @@ for t in "${platform_tarballs[@]}"; do publish_one "$t"; done if [ "${DRY_RUN}" != "1" ]; then echo "publish.sh: waiting for platform packages to propagate" - for t in "${platform_tarballs[@]}"; do - wait_visible "${NAME_OF[$t]}" "${VERSION_OF[$t]}" + for i in "${!platform_tarballs[@]}"; do + wait_visible "${platform_names[$i]}" "${platform_versions[$i]}" done fi