#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ "${SITES_ENV_READY:-}" != "1" ]]; then exec "${script_dir}/sites-env.sh" -- "$0" "$@" fi require_shared=0 prepare_only=0 for argument in "$@"; do case "${argument}" in --require-shared) require_shared=1 ;; --prepare-store) prepare_only=1 ;; *) echo "Unknown pnpm installation option: ${argument}" >&2; exit 64 ;; esac done for tool in flock timeout node; do command -v "${tool}" >/dev/null || { echo "pnpm setup requires Linux flock, GNU timeout, and Node.js." >&2; exit 69; } done cache_seed=decision_unavailable store_scope=unknown store_state=unavailable report_store() { node "${script_dir}/pnpm-install.mjs" --report-store "${cache_seed}" "${store_scope}" "${store_state}" "${prepare_only}" } # Publish preparation failures too; the optional report never decides success. trap 'report_store || true' EXIT if [[ -n "${SITES_PNPM_BIN:-}" && -f "${SITES_PNPM_BIN}" && -r "${SITES_PNPM_BIN}" ]]; then pnpm_command=(node "${SITES_PNPM_BIN}") elif [[ "${require_shared}" == 1 ]]; then echo "[sites] the image-pinned pnpm is unavailable" >&2 exit 69 elif command -v corepack >/dev/null; then # Corepack honors the established project's pin even when PATH has a different # pnpm version. A bare pnpm is used only if Corepack is absent, then verified. pnpm_command=(corepack pnpm) elif command -v pnpm >/dev/null; then pnpm_command=(pnpm) else echo "This pnpm project requires pnpm 11.25.0 or Corepack." >&2 exit 69 fi runtime_root="${SITES_RUNTIME_ROOT:-${SITES_PROJECT_ROOT}/.sites-runtime}" private_store="${runtime_root}/pnpm-store" if [[ "${HOME}" != "${runtime_root}/home" || -L "${private_store}" ]]; then echo "Dependency setup requires a project-owned writable home and pnpm store." >&2 exit 78 fi # A single Node process owns the regular-file descriptors for both leases. # Closing its input (including this shell exiting) releases the project lock. coproc SITES_INSTALL_LOCKS { node "${script_dir}/pnpm-install.mjs" --hold-install-locks \ "${runtime_root}/install.lock" "${SITES_PNPM_SHARED_STORE:-}.seed.lock" \ "${SITES_PNPM_STORE_LOCK_TIMEOUT:-30}" } install_lock_pid="${SITES_INSTALL_LOCKS_PID}" install_lock_output="${SITES_INSTALL_LOCKS[0]}" install_lock_input="${SITES_INSTALL_LOCKS[1]}" lock_status=unavailable read -r lock_status <&"${install_lock_output}" || true if [[ "${lock_status}" != locked ]]; then exec {install_lock_input}>&- exec {install_lock_output}<&- wait "${install_lock_pid}" || true if [[ "${lock_status}" == busy ]]; then echo "Another dependency install is already running for ${SITES_PROJECT_ROOT}." >&2 exit 75 fi echo "Dependency setup requires a regular, writable project install lock." >&2 exit 78 fi export XDG_CACHE_HOME="${runtime_root}/xdg-cache" XDG_DATA_HOME="${runtime_root}/xdg-data" mkdir -p "${XDG_CACHE_HOME}" "${XDG_DATA_HOME}" || exit 70 pnpm_version="$(timeout --signal=TERM --kill-after="${SITES_INSTALL_KILL_AFTER:-15s}" \ "${SITES_PNPM_BOOTSTRAP_TIMEOUT:-30s}" "${pnpm_command[@]}" --version)" if [[ "${pnpm_version}" != "11.25.0" ]]; then echo "This project requires pnpm 11.25.0 and its v11 store format." >&2 exit 69 fi can_write_directory() { local probe # access()/test -w can succeed even when a kernel sandbox denies a write. probe="$(mktemp "${1}/.sites-store-probe.XXXXXX" 2>/dev/null)" || return 1 rm -f -- "${probe}" 2>/dev/null } release_shared_lock() { if [[ "${shared_lock_held:-}" == 1 ]]; then printf '%s\n' release-shared >&"${install_lock_input}" local status read -r status <&"${install_lock_output}" && [[ "${status}" == released ]] || return 70 shared_lock_held=0 fi } acquire_shared_lock() { # The Node holder opens this workspace-controlled entry with no-follow and # nonblocking flags, then verifies a regular file before bounded flock. printf '%s\n' shared >&"${install_lock_input}" local status if read -r status <&"${install_lock_output}" && [[ "${status}" == locked ]]; then shared_lock_held=1 return 0 fi return 1 } writable_store="${private_store}" shared_store="${SITES_PNPM_SHARED_STORE:-}" if [[ "${shared_store}" == "/workspace/.sites-runtime/pnpm-store" && ! -L "${shared_store}" && ! -L "${shared_store%/*}" && -d "${shared_store%/*/*}" ]]; then # Normal Work already writes this owner's workspace. Narrower profiles must # pass the actual create probe, without widening their permissions. if mkdir -p "${shared_store%/*}" 2>/dev/null && can_write_directory "${shared_store%/*}" && acquire_shared_lock; then if [[ ! -L "${shared_store}" && ! -L "${shared_store%/*}" && ( ! -e "${shared_store}" || ( -d "${shared_store}" && ! -L "${shared_store}" && -w "${shared_store}" ) ) ]] && { [[ ! -d "${shared_store}" ]] || can_write_directory "${shared_store}"; }; then writable_store="${shared_store}" store_scope=workspace else release_shared_lock fi else release_shared_lock fi fi if [[ "${store_scope}" != workspace ]]; then if [[ "${require_shared}" == 1 ]]; then echo "[sites] the workspace pnpm store is unavailable" >&2 exit 69 fi store_scope=project fi seed="${SITES_PNPM_CACHE_SEED:-}" cache_seed=seed_unavailable if [[ -d "${writable_store}" ]]; then # Never merge a newer image seed into an existing mutable store. store_state=reused cache_seed=not_applicable if [[ -f "${writable_store}/.sites-pnpm-seed-applied.json" ]]; then cache_seed=seed_used; fi else seed_compatible=0 if [[ -n "${seed}" && -d "${seed}/v11" ]] && node --input-type=module - "${seed}/.sites-pnpm-seed.json" <<'NODE' import { readFileSync, statSync } from "node:fs"; try { const filename = process.argv[2]; if (statSync(filename).size > 4096) process.exit(1); const seed = JSON.parse(readFileSync(filename, "utf8")); process.exit(seed.version === 1 && seed.pnpm_version === "11.25.0" && seed.store_version === "v11" && /^[a-f0-9]{64}$/.test(seed.lockfile_sha256) ? 0 : 1); } catch { process.exit(1); } NODE then seed_compatible=1; fi if [[ "${seed_compatible}" == 1 ]]; then if [[ "${prepare_only}" == 1 ]]; then echo "[sites] copying the image seed into the writable ${store_scope} pnpm store" >&2 else echo "[sites] copying the image seed into the writable ${store_scope} pnpm store" fi seed_stage="$(mktemp -d "${writable_store}.seed.XXXXXX")" || exit 70 trap 'if [[ -n "${seed_stage:-}" ]]; then rm -rf -- "${seed_stage}"; fi; report_store || true' EXIT timeout --signal=TERM --kill-after="${SITES_INSTALL_KILL_AFTER:-15s}" \ "${SITES_PNPM_STORE_PREPARE_TIMEOUT:-60s}" bash -c \ 'cp -a --no-preserve=ownership "$1/." "$2/" && chmod -R u+rwX "$2"' _ "${seed}" "${seed_stage}" || exit 70 cp "${seed}/.sites-pnpm-seed.json" "${seed_stage}/.sites-pnpm-seed-applied.json" || exit 70 # An ordinary pnpm command can initialize the store without our seed lock. # Publish without replacing even an empty directory created by that command. mv -T --update=none "${seed_stage}" "${writable_store}" || exit 70 if [[ -d "${seed_stage}" ]]; then if [[ -L "${writable_store}" || ! -d "${writable_store}" ]]; then exit 78; fi rm -rf -- "${seed_stage}" || exit 70 store_state=reused cache_seed=not_applicable if [[ -f "${writable_store}/.sites-pnpm-seed-applied.json" ]]; then cache_seed=seed_used; fi else store_state=seeded cache_seed=seed_used fi seed_stage="" else mkdir -p "${writable_store}" || exit 70 store_state=created fi fi if ! can_write_directory "${writable_store}"; then store_state=unavailable; exit 70; fi release_shared_lock report_store trap - EXIT if [[ "${prepare_only}" == 1 ]]; then exit 0; fi # Configuration travels with source without embedding an absolute machine path. # A later restricted session selects a private store before its frozen repair. configured_store=.sites-runtime/pnpm-store if [[ "${store_scope}" == workspace ]]; then configured_store='${SITES_PNPM_SHARED_STORE:-.sites-runtime/pnpm-store}' elif [[ "${runtime_root}" != "${SITES_PROJECT_ROOT}/.sites-runtime" ]]; then configured_store='${SITES_RUNTIME_ROOT:-.sites-runtime}/pnpm-store' fi "${pnpm_command[@]}" config set package-import-method clone-or-copy --location project "${pnpm_command[@]}" config set store-dir "${configured_store}" --location project "${pnpm_command[@]}" config set cache-dir "${configured_store}/policy-cache" --location project # CI=true lets native pnpm install rebuild modules whose store moved, without a # prompt, --force, changing the lockfile, or translating the project into npm. CI=true timeout --signal=TERM --kill-after="${SITES_INSTALL_KILL_AFTER:-15s}" \ "${SITES_INSTALL_TIMEOUT:-8m}" node "${script_dir}/pnpm-install.mjs" \ "${cache_seed}" "${store_scope}" "${store_state}" "${writable_store}" "${pnpm_command[@]}"