217 lines
8.8 KiB
Bash
Executable File
217 lines
8.8 KiB
Bash
Executable File
#!/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[@]}"
|