|
Server IP : 178.210.175.110 / Your IP : 216.73.216.115 Web Server : Apache System : Linux lin4.teknikdata.com 5.14.0-611.5.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Nov 11 08:09:09 EST 2025 x86_64 User : naciustu ( 1040) PHP Version : 8.2.35 Disable Function : exec,passthru,shell_exec,system MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF Directory (0755) : /../run/../etc/cl.python/../cl.python/../cl.python/../cl.python/../cl.python/../utils/ |
| [ Home ] | [ C0mmand ] | [ Upload File ] |
|---|
#!/usr/bin/bash
if [[ $EUID -eq 0 ]]; then
echo "This program is not intended to be run as root." 1>&2
exit 1
fi
error_msg="Cloudlinux NodeJS Selector demands to store node modules for application in separate folder \
(virtual environment) pointed by symlink called \"node_modules\". That's why application should not contain \
folder/file with such name in application root"
CWD=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
source "${CWD}/activate"
eval "$(${CWD}/set_env_vars.py nodejs)"
venv_node_modules="${CL_VIRTUAL_ENV}/lib/node_modules"
nodejs_npm="$CL_NODEHOME/usr/bin/npm"
# set_env_vars did not resolve this invocation to an application (its
# diagnostics go to stderr, so eval saw nothing): there is no venv to deliver
# into and the paths below would be nonsense like "$HOME//node_modules". Run
# plain npm and get out, before any bun-delivery logic.
if [[ -z "${CL_APP_ROOT:-}" ]]; then
exec "${nodejs_npm}" "$@"
fi
app_node_modules="${HOME}/${CL_APP_ROOT}/node_modules"
invalidate_shared_node_modules_marker() {
# A failed plain npm can leave a conservative false-negative; never claim
# the delivered tree is still intact once npm was allowed to mutate it.
rm -f "${venv_node_modules}/.cl-shared-node-modules.json"
}
# bun install-time delivery (docs/design/nodejs-bun-delivery.md).
#
# This wrapper is where an account's own `npm install` lands -- a person
# over SSH, or an agent in a shell, which is the workload a shared cache
# exists for: an agent redeploys dozens of times an afternoon and every one
# of those installs re-downloads what the server already has.
#
# The account cannot link out of the cache itself (the kernel refuses a hard
# link to a file it does not own), so the middle phase is asked of a root
# daemon over a unix socket. The account keeps the phases that must stay
# unprivileged: resolution runs with its own registry configuration, and the
# native builds run as itself.
#
# resolve (account) -> deliver (root, over the socket) -> rebuild (account)
#
# If any step fails -- daemon stopped, a busy application -- the install
# fails and says why, rather than quietly becoming a plain npm install.
# Explicit administrator disable/exclusion policy selects classic npm, and so
# does an input this engine cannot serve (a resolve sentinel, or a source or
# package shape root refused), each named on stderr;
# every other refusal remains loud. Silently
# choosing a different installer is what let two delivery defects
# sit behind healthy-looking numbers while this feature was built; an install that looks like it
# succeeded is worse than one that stops.
BUN_USER_PHASE="/usr/share/l.v.e-manager/utils/bun_user_phase"
BUN_DELIVERY_CONF="/opt/cl-bun/etc/node-modules-storage.json"
NODE_MODULES_STORAGE_PYTHON="/opt/cloudlinux/venv/bin/python3"
NODE_MODULES_STORAGE_READER="${NODE_MODULES_STORAGE_PYTHON}"
DELIVERY_CLIENT="/usr/share/l.v.e-manager/utils/cloudlinux_bun_client.py"
close_selector_lock_copy_for_npm() {
# Selector itself keeps the same open-file-description locked until the
# npm operation and its restart/health phases finish. Drop only this
# wrapper child's descriptor before npm starts, otherwise a lifecycle
# script that backgrounds a process could inherit the application lock
# indefinitely after Selector returns.
local selector_fd="${CL_SELECTOR_APP_LOCK_FD:-}"
local lock_file="$(dirname "${CL_VIRTUAL_ENV}")/.lock"
[[ "${selector_fd}" =~ ^[0-9]+$ ]] || return 0
[ -e "/proc/self/fd/${selector_fd}" ] || return 0
[ "$(stat -Lc '%d:%i' "/proc/self/fd/${selector_fd}" 2>/dev/null)" \
= "$(stat -Lc '%d:%i' "${lock_file}" 2>/dev/null)" ] || return 0
eval "exec ${selector_fd}>&-"
}
discard_restored_private_marker() {
# Account backups materialize hardlinks as ordinary private files but may
# copy the delivery marker. With no server configuration, discard only a
# regular account-owned marker after a bounded walk proves the whole tree
# contains no shared regular inode or unsafe node type. A restored
# root-owned single-link file is private and npm can replace its directory
# entry without gaining write access to the inode.
# Any ambiguity leaves the marker in place for privileged fail-closed
# unshare handling below.
local modules="${venv_node_modules}"
local marker="${modules}/.cl-shared-node-modules.json"
local account_uid unsafe scan_status
[ ! -e "${BUN_DELIVERY_CONF}" ] || return 1
[ -d "${modules}" ] && [ ! -L "${modules}" ] || return 1
[ -f "${marker}" ] && [ ! -L "${marker}" ] || return 1
account_uid=$(id -u) || return 1
[ "$(stat -c '%u' -- "${marker}" 2>/dev/null)" = "${account_uid}" ] \
|| return 1
command -v timeout >/dev/null 2>&1 || return 1
# Only a foreign-owned linked inode can be a store inode: with
# fs.protected_hardlinks=1 the account cannot link a file it does not
# own, so its own linked bytes (`cp -al`, `rsync -aH` restores) are
# private however many links they carry.
unsafe=$(timeout 5 find "${modules}" -xdev \
\( \( -type f -links +1 ! -uid "${account_uid}" \) \
-o \( ! -type d ! -type f ! -type l \) \) \
-print -quit 2>/dev/null)
scan_status=$?
[ "${scan_status}" -eq 0 ] && [ -z "${unsafe}" ] || return 1
rm -f -- "${marker}"
}
# A self-contained account restore has private bytes even when its copied
# telemetry marker says the source application used the Store. Retire that
# stale marker only after the private-tree proof above; a failed proof keeps
# the Shared Store-aware recovery path.
if [ ! -e "${BUN_DELIVERY_CONF}" ] \
&& [ -e "${venv_node_modules}/.cl-shared-node-modules.json" ]; then
discard_restored_private_marker || true
fi
unverified_sharing_scan() {
# The bounded scan could not answer and there is no marker. Failing
# closed here would put every npm call of a markerless application on
# the heavy path -- including root unshare and its budget -- on every
# disabled-config host, and under LVE IO a large tree loses that race
# routinely. Only a configuration that may enable delivery keeps the
# conservative verdict.
config_may_enable_delivery && return 0
return 1
}
scan_shared_tree() {
local modules="${CL_VIRTUAL_ENV}/lib/node_modules"
local account_uid shared_file scan_status
[ -e "${modules}/.cl-shared-node-modules.json" ] && return 0
[ -d "${modules}" ] || return 1
command -v timeout >/dev/null 2>&1 || { unverified_sharing_scan; return $?; }
account_uid=$(id -u) || { unverified_sharing_scan; return $?; }
# pnpm, `cp -l` and backup tools may create account-owned hardlinks.
# Shared Store cache inodes stay foreign (normally root-owned), so only
# those ambiguous links need privileged fail-closed recovery.
shared_file=$(timeout 5 find "${modules}" -xdev -type f -links +1 \
! -uid "${account_uid}" \
-print -quit 2>/dev/null)
scan_status=$?
[ "${scan_status}" -eq 0 ] || { unverified_sharing_scan; return $?; }
[ -n "${shared_file}" ]
}
shared_tree_verdict=''
shared_tree_may_exist() {
# One scan per process: the compatibility gate and the pre-unshare check
# ask the same question, and the answer is a bounded walk of the whole
# tree. Nothing between them changes this tree -- the app lock is held
# before the second caller runs.
if [ -z "${shared_tree_verdict}" ]; then
scan_shared_tree
shared_tree_verdict=$?
fi
return "${shared_tree_verdict}"
}
config_may_enable_delivery() {
[ -e "${BUN_DELIVERY_CONF}" ] || return 1
[ -r "${BUN_DELIVERY_CONF}" ] || return 0
grep -q '"enabled"[[:space:]]*:[[:space:]]*true' "${BUN_DELIVERY_CONF}" 2>/dev/null || return 1
return 0
}
is_bare_npm_ci_spelling() {
case "${1:-}" in
ci|clean-install|ic|install-clean|isntall-clean) return 0 ;;
esac
return 1
}
is_npm_ci() {
is_bare_npm_ci_spelling "$1" && return 0
case "${1:-}" in
install-ci-test|cit|clean-install-test|sit) return 0 ;;
esac
return 1
}
expose_app_lockfiles_for_ci() {
# `npm ci` installs exactly the pins of the application's lockfile, and
# with the venv as its prefix npm reads that lockfile -- and its project
# `.npmrc` -- from venv lib. When the application root has a regular npm
# lockfile, venv lib names exactly it: a differing copy an earlier prefix
# install wrote there describes the tree `npm ci` replaces. A linked
# application lockfile is a historical layout whose target may be the
# venv copy itself, so it is never followed into a deletion. Without a
# regular one, npm reads what venv lib has -- the pins of the previous
# install there -- or fails with its own error. The application's
# regular `.npmrc` is linked the same way unless venv lib already has
# one, which the account wrote and keeps.
local name app_file venv_file app_has_lock=false
for name in npm-shrinkwrap.json package-lock.json; do
app_file="${HOME}/${CL_APP_ROOT}/${name}"
[[ -f "${app_file}" && ! -L "${app_file}" ]] && app_has_lock=true
done
for name in npm-shrinkwrap.json package-lock.json; do
[ "${app_has_lock}" = true ] || break
app_file="${HOME}/${CL_APP_ROOT}/${name}"
venv_file="${CL_VIRTUAL_ENV}/lib/${name}"
[[ "${app_file}" -ef "${venv_file}" ]] && continue
rm -f -- "${venv_file}" || return 1
if [[ -f "${app_file}" && ! -L "${app_file}" ]]; then
ln -s -- "${app_file}" "${venv_file}" || return 1
fi
done
app_file="${HOME}/${CL_APP_ROOT}/.npmrc"
venv_file="${CL_VIRTUAL_ENV}/lib/.npmrc"
if [[ -f "${app_file}" && ! -L "${app_file}" \
&& ! -e "${venv_file}" && ! -L "${venv_file}" ]]; then
ln -s -- "${app_file}" "${venv_file}" || return 1
fi
}
ci_aside=''
set_aside_app_node_modules_for_ci() {
# The self-healing half of the CLOS-7870 gap: `npm ci` run in the
# application root before CLOS-7874 left a real node_modules there,
# which every Selector install refuses. `npm ci` replaces node_modules
# wholesale anyway, so that tree moves aside for the venv link; it is
# removed once npm succeeded and put back if anything fails before.
[[ -d "${app_node_modules}" && ! -L "${app_node_modules}" ]] || return 0
# A SIGKILLed run (no trap) leaves its copy behind, and a later one may
# get the same pid: never move a tree into it. The real directory then
# stays, and the link check refuses as it always did.
[[ ! -e "${app_node_modules}.cl-ci-$$" && ! -L "${app_node_modules}.cl-ci-$$" ]] \
|| return 0
ci_aside="${app_node_modules}.cl-ci-$$"
mv -T -- "${app_node_modules}" "${ci_aside}" || { ci_aside=''; return 1; }
trap restore_app_node_modules_for_ci EXIT
}
restore_app_node_modules_for_ci() {
[ -n "${ci_aside}" ] || return 0
[ ! -L "${app_node_modules}" ] || rm -f -- "${app_node_modules}"
mv -- "${ci_aside}" "${app_node_modules}"
}
run_legacy_npm() {
# This is the exact pre-Shared-Store wrapper contract: only the historical
# setup/list spellings target the Selector venv; everything else reaches
# npm unchanged. Keep it in one helper so an absent and a disabled config
# cannot drift apart again.
#
# Except `npm ci`: passed through, it replaced the application root's
# node_modules link with a real directory, after which every Selector
# install refused the application. It returns to take the locked setup
# path below, as it does wherever the Store may be in use. Its test
# variants stay with `install-test`, which this contract passes through.
is_bare_npm_ci_spelling "${1:-}" && return 0
if [[ "$*" =~ ^(install|i|add|list|la|ll)$ \
|| "$*" =~ ^(install|i|add|list|la|ll)[[:space:]].*$ ]]; then
if [[ -L "${app_node_modules}" ]]; then
rm -f "${app_node_modules}" \
|| { echo "Can't remove symlink ${app_node_modules}" >&2; exit 1; }
elif [[ -d "${app_node_modules}" || -f "${app_node_modules}" ]]; then
echo "${error_msg}" >&2
exit 1
fi
mkdir -p "${venv_node_modules}" || exit 1
ln -fs "${venv_node_modules}" "${app_node_modules}" || exit 1
ln -sf "${HOME}/${CL_APP_ROOT}/package.json" \
"${CL_VIRTUAL_ENV}/lib/package.json" || exit 1
close_selector_lock_copy_for_npm
exec "${nodejs_npm}" "$@" --prefix="${CL_VIRTUAL_ENV}/lib"
fi
close_selector_lock_copy_for_npm
exec "${nodejs_npm}" "$@"
}
# Default-off compatibility is an explicit early branch. On a server where
# delivery is not enabled and the application has no marker or shared inode,
# preserve the wrapper shipped before this feature: only setup/list commands
# target the Selector venv; every other npm command is an untouched exec. A
# config written only to change limits must not permanently wrap every app.
# Ambiguous or actually shared trees remain on the fail-closed recovery path.
if [ ! -e "${BUN_DELIVERY_CONF}" ] \
&& [ ! -e "${venv_node_modules}/.cl-shared-node-modules.json" ]; then
run_legacy_npm "$@"
fi
# An administrator may publish a disabled config merely to tune limits. That
# must not opt every untouched application into the new lock/prepare path.
# Only a delivery marker or a foreign-owned linked inode needs the
# compatibility/recovery path below. Canonical Selector links and lockfiles
# predate Shared Store and must not opt an ordinary application into it.
if [ -e "${BUN_DELIVERY_CONF}" ] \
&& ! config_may_enable_delivery \
&& ! shared_tree_may_exist; then
run_legacy_npm "$@"
fi
bun_delivery_state() {
# 0 means ask the authenticated daemon for this account's policy, 1 means
# the plain npm path because the global feature is off, and 2 means the
# public configuration reader could not run. Account names are never
# present in this CageFS-readable file.
#
# Refuse-on-error exists so a delivery that should happen is never
# silently skipped -- which is a reason only where the config plausibly
# says enabled. The file is written by cl-node-modules-storage alone,
# so absent, or with no plain `"enabled": true` in it, is the fleet
# that never turned the feature on: that is decided right here in
# bash, and a broken interpreter or venv cannot turn every account's
# `npm install` into a refusal there. Anything that *looks* enabled
# goes to the authoritative reader (a nested "enabled" in an unknown
# key must not enable the feature by grep), and only there does a
# reader that cannot run refuse.
[ -e "$1" ] || return 1
grep -q '"enabled"[[:space:]]*:[[:space:]]*true' "$1" 2>/dev/null || return 1
# Isolated mode keeps the account's environment and working directory
# out of Python imports.
"${NODE_MODULES_STORAGE_READER}" -Ibb -c '
import sys
try:
from clselector import node_modules_storage
config = node_modules_storage.read_public_config(sys.argv[1])
applies = config["enabled"] and node_modules_storage.nodejs_selector_supported()
except Exception as error:
sys.stderr.write("Node modules storage configuration error: %s\\n" % error)
sys.exit(2)
sys.exit(0 if applies else 1)
' "$1"
}
bun_delivery_run() (
lock_classic || return $?
# A freshly created application has only <venv>/<major>/bin, and the
# daemon finds the venv by its library directory: without this the very
# first `npm install` over SSH is refused before anything is installed.
# The classic and resolve paths create the same account-owned directory.
mkdir -p "${CL_VIRTUAL_ENV}/lib" || return 1
"${NODE_MODULES_STORAGE_PYTHON}" "${DELIVERY_CLIENT}" \
policy "${CL_APP_ROOT}"
policy_status=$?
[ "${policy_status}" -eq 0 ] || return "${policy_status}"
# A Store `npm ci` resolves the application's own lockfile.
if [ "${CL_BUN_COMMAND:-}" = ci ]; then
expose_app_lockfiles_for_ci || return 1
fi
# The phase keeps stdout clean for exactly these sentinels and says
# nothing about them on stderr; bun's own diagnosis of a failed install
# resolve goes to stderr and reaches the account unchanged.
resolve_sentinel="$("${BUN_USER_PHASE}" resolve "${CL_APP_ROOT}")"
resolve_status=$?
# Exit 3 is the classic-npm route, but the phase also propagates bun's
# own status: without the sentinel a future bun exit 3 would send every
# account to classic npm silently. The code alone is not the contract.
if [ "${resolve_status}" -eq 3 ] \
&& { [ "${resolve_sentinel}" = 'cl-bun:legacy-package-lock-v1' ] \
|| [ "${resolve_sentinel}" = 'cl-bun:unsupported-npm-shrinkwrap' ] \
|| [ "${resolve_sentinel}" = 'cl-bun:no-dependencies' ] \
|| case "${resolve_sentinel}" in
'cl-bun:unsupported-manifest '*) true ;;
*) false ;;
esac; }; then
return 3
fi
if [ "${resolve_status}" -ne 0 ] && [ "${CL_BUN_COMMAND:-}" = ci ]; then
# A drifted, missing or unreadable lockfile is npm's to refuse, with
# its own error -- or to install, where only bun could not.
return 3
fi
if [ "${resolve_status}" -ne 0 ]; then
# The phase has already printed bun's own diagnosis; name the phase
# so the account knows npm never ran and the tree is unchanged.
echo "Shared node_modules Store: resolve phase failed;" \
"npm install was not run" 1>&2
return 1
fi
# Delivery now keeps the old tree in its privileged transaction while it
# invokes the account-side rebuild; only a successful rebuild commits.
# The client's exit 3 is its own (never bun's): root refused a source or
# package shape it does not serve with the tree untouched -- or answered
# disabled/excluded after a policy change since the preflight -- and the
# install takes the classic route. Like the no-dependencies sentinel,
# that route must not leave resolve's native-build settings for a
# rebuild that never runs. The client names such a reason on stdout; an
# `npm ci` refused for one is remembered (ci_classic_remembered).
deliver_reason="$("${BUN_USER_PHASE}" deliver "${CL_APP_ROOT}")"
case $? in
0)
# Delivered: an earlier refusal's memo is stale.
rm -f -- "${CI_CLASSIC_MEMO}"
return 0
;;
3)
rm -f -- "${CL_VIRTUAL_ENV}/lib/.cl-bun-rebuild-env"
if [ "${CL_BUN_COMMAND:-}" = ci ] && [ -n "${deliver_reason}" ]; then
ci_input_hashes > "${CI_CLASSIC_MEMO}" \
|| rm -f -- "${CI_CLASSIC_MEMO}"
fi
return 3
;;
esac
return 1
)
CI_CLASSIC_MEMO="${CL_VIRTUAL_ENV}/lib/.cl-bun-ci-classic"
ci_input_hashes() {
# The Store configuration too: an allowlist change may serve what a
# foreign registry made the Store refuse.
sha256sum -- "${HOME}/${CL_APP_ROOT}/package-lock.json" \
"${HOME}/${CL_APP_ROOT}/package.json" "${BUN_DELIVERY_CONF}" 2>/dev/null
}
ci_classic_remembered() {
# An application whose lockfile the Store refused for good -- inexact,
# a git or bundled dependency, a foreign registry -- would pay a
# delivery from the account's budget on every `npm ci` and end at
# rate_limited. The account-owned memo only narrows the account's own
# Store use: while the lockfile and manifest hash as they did when root
# refused them, `npm ci` goes to classic npm without asking the daemon.
local hashes
[ -f "${CI_CLASSIC_MEMO}" ] && [ ! -L "${CI_CLASSIC_MEMO}" ] || return 1
hashes="$(ci_input_hashes)" || return 1
[ "${hashes}" = "$(cat -- "${CI_CLASSIC_MEMO}" 2>/dev/null)" ]
}
bun_delivery_needs_unshare() {
# A marker or linked inode proves Shared Store may have created this tree.
# A bounded-scan failure stays shared/fail-closed only while the
# configuration may enable delivery; see unverified_sharing_scan.
shared_tree_may_exist
}
lock_classic() {
# All setup and classic mutation paths take this descriptor before they
# inspect or alter the app-root node_modules link. Direct SSH npm inherits
# this wrapper-owned descriptor; a Selector child drops only its inherited
# duplicate before npm because the Selector parent keeps the same OFD.
#
# CL_BUN_VIA=selector tells the daemon this install was Selector-driven
# (the account's panel UI runs Selector as the account and lands here),
# so it is counted as a panel install, not an SSH one. It is set only once
# the descriptor proved to be this application's lock; an account that
# forges it moves only the attribution of an install it still pays for.
local lock_file
lock_file="$(dirname "${CL_VIRTUAL_ENV}")/.lock"
unset CL_BUN_VIA
if [[ "${CL_SELECTOR_APP_LOCK_FD:-}" =~ ^[0-9]+$ ]] \
&& [ -e "/proc/self/fd/${CL_SELECTOR_APP_LOCK_FD}" ] \
&& [ "$(stat -Lc '%d:%i' "/proc/self/fd/${CL_SELECTOR_APP_LOCK_FD}" 2>/dev/null)" \
= "$(stat -Lc '%d:%i' "${lock_file}" 2>/dev/null)" ]; then
bun_lock_fd="${CL_SELECTOR_APP_LOCK_FD}"
flock -n "${bun_lock_fd}" \
|| { echo "application is locked by another operation" >&2; return 1; }
export CL_BUN_LOCK_FD="${bun_lock_fd}"
export CL_BUN_VIA=selector
return 0
fi
exec {bun_lock_fd}>"${lock_file}" || return 1
flock -n "${bun_lock_fd}" || { echo "application is locked by another operation" >&2; return 1; }
export CL_BUN_LOCK_FD="${bun_lock_fd}"
}
lock_and_prepare_classic() {
lock_classic || return $?
# The common user phase canonicalizes unambiguous legacy prefix lockfiles
# while this app lock is held. Divergent regular copies are preserved for
# classic npm. Do this before scanning or asking root to unshare.
classic_canonical_locks=""
classic_prepare_needed=false
if [ ! -e "${BUN_DELIVERY_CONF}" ] \
&& [ ! -e "${venv_node_modules}/.cl-shared-node-modules.json" ]; then
return 0
fi
classic_prepare_needed=true
"${BUN_USER_PHASE}" prepare-classic "${CL_APP_ROOT}" || return 1
local lockfile app_lock venv_lock
for lockfile in package-lock.json npm-shrinkwrap.json yarn.lock; do
app_lock="${HOME}/${CL_APP_ROOT}/${lockfile}"
venv_lock="${CL_VIRTUAL_ENV}/lib/${lockfile}"
if [[ -L "${venv_lock}" && "$(readlink "${venv_lock}")" == "${app_lock}" ]]; then
classic_canonical_locks="${classic_canonical_locks} ${lockfile}"
fi
done
}
finish_classic_lockfiles() {
# npm writes prefix lockfiles with rename(2), replacing a canonical
# symlink with a regular file. Only aliases observed after the locked
# preflight are authoritative here; a historical divergent pair was not
# an alias and remains untouched for classic compatibility.
local lockfile app_lock venv_lock
[ "${classic_prepare_needed:-false}" = true ] || return 0
# Validate every observed alias before moving any npm output. A malformed
# later lock must not leave an earlier app-root lock partly updated.
for lockfile in ${classic_canonical_locks:-}; do
app_lock="${HOME}/${CL_APP_ROOT}/${lockfile}"
venv_lock="${CL_VIRTUAL_ENV}/lib/${lockfile}"
if [[ -L "${venv_lock}" ]]; then
[[ "$(readlink "${venv_lock}")" == "${app_lock}" ]] || return 1
elif [[ -f "${venv_lock}" ]]; then
[[ ! -L "${app_lock}" && ( ! -e "${app_lock}" || -f "${app_lock}" ) ]] || return 1
elif [[ -e "${venv_lock}" ]]; then
return 1
elif [[ -e "${app_lock}" || -L "${app_lock}" ]]; then
[[ -f "${app_lock}" && ! -L "${app_lock}" ]] || return 1
fi
done
for lockfile in ${classic_canonical_locks:-}; do
app_lock="${HOME}/${CL_APP_ROOT}/${lockfile}"
venv_lock="${CL_VIRTUAL_ENV}/lib/${lockfile}"
if [[ -L "${venv_lock}" ]]; then
[[ "$(readlink "${venv_lock}")" == "${app_lock}" ]] || return 1
continue
fi
if [[ -f "${venv_lock}" ]]; then
[[ ! -L "${app_lock}" && ( ! -e "${app_lock}" || -f "${app_lock}" ) ]] || return 1
mv -f -- "${venv_lock}" "${app_lock}" || return 1
ln -s -- "${app_lock}" "${venv_lock}" || return 1
elif [[ -e "${venv_lock}" ]]; then
return 1
elif [[ -f "${app_lock}" && ! -L "${app_lock}" ]]; then
ln -s -- "${app_lock}" "${venv_lock}" || return 1
elif [[ -e "${app_lock}" || -L "${app_lock}" ]]; then
return 1
fi
done
"${BUN_USER_PHASE}" prepare-classic "${CL_APP_ROOT}"
}
bun_unshare_before_classic() {
# A classic npm mutation and the optional unshare are one transaction:
# take the same app lock before even scanning, then let exec npm inherit
# this descriptor. This keeps Selector/delivery from swapping the tree
# while npm changes it.
lock_and_prepare_classic || return $?
unshare_after_classic_lock
}
unshare_after_classic_lock() {
bun_delivery_needs_unshare || return 0
"${NODE_MODULES_STORAGE_PYTHON}" "${DELIVERY_CLIENT}" unshare "${CL_APP_ROOT}"
}
validate_setup_node_modules_link() {
if [[ -L "${app_node_modules}" ]]; then
if [[ "$(readlink "${app_node_modules}")" != "${venv_node_modules}" ]]; then
echo "${error_msg}" >&2
return 1
fi
elif [[ -e "${app_node_modules}" ]]; then
echo "${error_msg}" >&2
return 1
fi
}
ensure_setup_node_modules_link() {
validate_setup_node_modules_link || return $?
if [[ ! -e "${app_node_modules}" && ! -L "${app_node_modules}" ]]; then
mkdir -p "${venv_node_modules}" || return 1
ln -s "${venv_node_modules}" "${app_node_modules}" || return 1
fi
ln -sf "${HOME}/${CL_APP_ROOT}/package.json" "${CL_VIRTUAL_ENV}/lib/package.json"
}
is_npm_command_name() {
# npm accepts configuration flags before its command. Keep this list
# broad enough to distinguish an option value from the command that
# follows it; only the mutation classifiers below change behavior.
case "${1:-}" in
access|add|adduser|audit|bugs|cache|ci|cit|clean-install|clean-install-test|completion|config|ddp|dedupe|deprecate|diff|dist-tag|docs|doctor|edit|exec|explain|explore|find-dupes|fund|get|help|help-search|hook|i|ic|in|init|ins|inst|insta|instal|install|install-ci-test|install-clean|install-test|isnt|isnta|isntal|isntall|isntall-clean|it|la|link|list|ll|ln|login|logout|ls|org|outdated|owner|pack|ping|pkg|prefix|profile|prune|publish|query|rb|r|rebuild|remove|repo|restart|rm|root|run|run-script|s|se|search|set|shrinkwrap|sit|star|stars|start|stop|t|team|test|token|tst|un|uninstall|unlink|unpublish|unstar|up|update|upgrade|udpate|version|view|whoami|x)
return 0
;;
esac
return 1
}
npm_option_takes_value() {
# Options whose separate value may itself look like an npm command need
# an explicit entry. Other unknown leading options are handled below by
# consuming a following non-command token as their probable value.
case "${1:-}" in
-C|-w|--access|--before|--cache|--cafile|--cert|--cpu|--fetch-retries|--fetch-retry-factor|--fetch-retry-maxtimeout|--fetch-retry-mintimeout|--fetch-timeout|--https-proxy|--include|--install-strategy|--key|--libc|--location|--loglevel|--maxsockets|--node-options|--omit|--otp|--os|--prefix|--proxy|--registry|--scope|--script-shell|--tag|--userconfig|--workspace)
return 0
;;
esac
return 1
}
classify_npm_command_args() {
# Preserve the original argv for npm. Strip only leading global options
# from the copy used to decide whether the invocation mutates the tree.
local option
while [ "$#" -gt 0 ]; do
case "$1" in
--)
shift
break
;;
--*=*|-?=*)
shift
;;
-*)
option=$1
shift
if npm_option_takes_value "${option}"; then
[ "$#" -gt 0 ] && shift
elif [ "$#" -gt 0 ] && [[ "$1" != -* ]] \
&& ! is_npm_command_name "$1"; then
# npm configuration keys may be supplied as
# `--key value`. An unknown non-command token is the
# value; continue looking for the actual command.
shift
fi
;;
*)
break
;;
esac
done
npm_classification_args=("$@")
}
inspect_npm_mode_options() {
# These options change whether npm can touch this application's
# node_modules at all. Inspect the original argv up to `--`; unlike the
# command classifier, this does not need to remove or reorder anything.
npm_global_mode=false
npm_preview_mode=false
npm_lockfile_only=false
npm_explicit_prefix=false
# npm takes the last prefix it is given; so do we, or an account could
# name this venv first and something else last and be handled as this
# application while npm installed elsewhere (or the reverse).
npm_prefix_value=
while [ "$#" -gt 0 ]; do
case "$1" in
--)
break
;;
-g|--global|--global=true|--location=global)
npm_global_mode=true
;;
--location)
shift
if [ "${1:-}" = global ]; then
npm_global_mode=true
fi
;;
--prefix|-C)
npm_explicit_prefix=true
shift
npm_prefix_value="${1:-}"
;;
--prefix=*|-C=*)
npm_explicit_prefix=true
npm_prefix_value="${1#*=}"
;;
--dry-run|--dry-run=true|--help|--help=true|-h|-\?)
npm_preview_mode=true
;;
--package-lock-only|--package-lock-only=true)
npm_lockfile_only=true
;;
esac
[ "$#" -gt 0 ] && shift
done
}
npm_prefix_targets_application() {
# An explicit prefix that resolves to this application's venv IS this
# application, whatever npm calls it: `npm install --prefix
# ~/nodevenv/app/22/lib pkg` mutates exactly the tree a bare install
# does. Treating every explicit prefix as "somewhere else" let that
# through with no application lock, no unshare of a still-shared tree,
# and a marker left claiming the tree was intact.
local prefix="${1:-}" resolved venv_resolved
[ -n "${prefix}" ] || return 1
[ -n "${CL_VIRTUAL_ENV:-}" ] || return 1
# `realpath` is coreutils and is in every cage. If it is not, we cannot
# tell the two apart, and the safe answer is the locked application path
# rather than an unprotected mutation of a possibly shared tree.
resolved="$(realpath -m -- "${prefix}" 2>/dev/null)" || return 0
venv_resolved="$(realpath -m -- "${CL_VIRTUAL_ENV}" 2>/dev/null)" || return 0
[ -n "${resolved}" ] && [ -n "${venv_resolved}" ] || return 0
case "${resolved}" in
"${venv_resolved}"|"${venv_resolved}"/*)
return 0
;;
esac
return 1
}
is_setup_command() {
case "${1:-}" in
install|i|in|ins|inst|insta|instal|isnt|isnta|isntal|isntall|add|ci|clean-install|ic|install-clean|isntall-clean|install-test|it|install-ci-test|cit|clean-install-test|sit)
return 0
;;
esac
return 1
}
is_read_only_setup_command() {
case "${1:-}" in
list|la|ll) return 0 ;;
esac
return 1
}
is_shared_node_modules_install() {
case "${1:-}" in
install|i) [ "$#" -eq 1 ]; return ;;
esac
is_bare_npm_ci_spelling "${1:-}" || return 1
shift
# Output-only flags change nothing the delivery installs; npm still gets
# them unchanged whenever the route ends on classic npm.
while [ "$#" -gt 0 ]; do
case "$1" in
--no-audit|--no-fund|--silent|--quiet|--no-progress|--progress=false|--loglevel=*) ;;
--loglevel) shift; [ "$#" -gt 0 ] || return 1 ;;
*) return 1 ;;
esac
shift
done
}
is_classic_npm_mutation() {
case "${1:-}" in
install|i|in|ins|inst|insta|instal|isnt|isnta|isntal|isntall|add|ci|clean-install|ic|install-clean|isntall-clean|install-test|it|install-ci-test|cit|clean-install-test|sit|update|upgrade|up|udpate|uninstall|unlink|remove|rm|r|un|prune|dedupe|ddp|link|ln|rebuild|rb)
return 0
;;
audit)
local audit_arg
shift
for audit_arg in "$@"; do
case "$audit_arg" in
fix|--fix) return 0 ;;
esac
done
return 1
;;
esac
return 1
}
# Only the Store route below exports CL_BUN_COMMAND=ci; an inherited value
# must not narrow anything else.
unset CL_BUN_COMMAND
classify_npm_command_args "$@"
inspect_npm_mode_options "$@"
# Global npm and a prefix outside this venv have a different target; help/dry-run
# does not change this application tree. Preserve npm's original argv and avoid
# any application lock, unshare, marker, link, or prefix side effect.
npm_explicit_application_prefix=false
if [ "${npm_explicit_prefix}" = true ] \
&& npm_prefix_targets_application "${npm_prefix_value}"; then
npm_explicit_application_prefix=true
fi
if [ "${npm_global_mode}" = true ] \
|| [ "${npm_preview_mode}" = true ] \
|| { [ "${npm_explicit_prefix}" = true ] \
&& [ "${npm_explicit_application_prefix}" = false ]; }; then
close_selector_lock_copy_for_npm
exec "${nodejs_npm}" "$@"
fi
# Everything below runs npm against this application. It normally names the
# venv library as the prefix itself; when the account already named a prefix
# inside this venv, npm keeps the argv it was given -- appending a second
# --prefix would silently retarget theirs.
npm_prefix_args=(--prefix="${CL_VIRTUAL_ENV}/lib")
if [ "${npm_explicit_application_prefix}" = true ]; then
npm_prefix_args=()
fi
# package-lock-only changes dependency metadata but not node_modules. Keep it
# serialized with delivery and canonicalize unambiguous legacy lock inputs,
# but do not detach a shared tree or invalidate its marker.
if [ "${npm_lockfile_only}" = true ] \
&& is_classic_npm_mutation "${npm_classification_args[@]}"; then
lock_and_prepare_classic || exit $?
# npm resolves the selected prefix from venv lib, so expose the same
# application manifest as the ordinary setup path without creating or
# detaching node_modules.
ln -sf "${HOME}/${CL_APP_ROOT}/package.json" \
"${CL_VIRTUAL_ENV}/lib/package.json" || exit 1
close_selector_lock_copy_for_npm
"${nodejs_npm}" "$@" "${npm_prefix_args[@]}"
npm_status=$?
finish_classic_lockfiles
prepare_status=$?
if [ "${npm_status}" -ne 0 ]; then
exit "${npm_status}"
fi
exit "${prepare_status}"
fi
# Commands that populate the venv have a common setup path. Only the two
# canonical bare install spellings and a bare `npm ci` (with its four
# aliases) can use Bun delivery; every other alias remains a classic npm
# mutation under the app lock.
if is_read_only_setup_command "${npm_classification_args[@]}"; then
# Metadata queries can run while Selector owns the application flock.
# They must not canonicalize inputs, alter links, or detach a tree.
close_selector_lock_copy_for_npm
exec "${nodejs_npm}" "$@" "${npm_prefix_args[@]}"
elif is_setup_command "${npm_classification_args[@]}"; then
# A bare `install`/`i` is the whole tree, which is what the cache can
# serve, and so is a bare `npm ci` when root proves it exact. Named
# packages, uninstall and update change package.json first and stay on
# the plain path, like any argument.
if is_shared_node_modules_install "$@"; then
bun_delivery_state "${BUN_DELIVERY_CONF}"
delivery_state=$?
case "${delivery_state}" in
0)
# Resolve and the client deliver `npm ci` exactly or not at
# all.
is_npm_ci "$1" && export CL_BUN_COMMAND=ci
if [ "${CL_BUN_COMMAND:-}" != ci ] || ! ci_classic_remembered; then
bun_delivery_run
delivery_status=$?
[ "${delivery_status}" -eq 3 ] || exit "${delivery_status}"
fi
;;
1)
;;
*)
echo "Node modules storage could not be checked; refusing to run npm." 1>&2
exit "${delivery_state}"
;;
esac
fi
if is_classic_npm_mutation "${npm_classification_args[@]}"; then
lock_and_prepare_classic || exit $?
if is_npm_ci "${npm_classification_args[@]}"; then
set_aside_app_node_modules_for_ci || exit 1
fi
validate_setup_node_modules_link || exit $?
# npm 10's `ci` removes every entry of node_modules before it
# installs and never writes into an existing inode: copying a
# delivered tree private first would only feed npm's removal. A
# refusal before that removal leaves the tree shared and its marker
# valid; a failure part-way leaves the marker, which sends the next
# mutation through the unshare below (shared_tree_may_exist).
if ! is_npm_ci "${npm_classification_args[@]}"; then
unshare_after_classic_lock || exit $?
invalidate_shared_node_modules_marker
fi
else
lock_classic || exit $?
fi
ensure_setup_node_modules_link || exit $?
if is_npm_ci "${npm_classification_args[@]}"; then
expose_app_lockfiles_for_ci || exit 1
fi
# With no existing lock input npm must create a regular prefix lock:
# npm 10 rejects a dangling package-lock symlink. Keep this shell (and
# therefore the application lock) around long enough to move the new
# lock to the canonical app root and replace it with the venv alias.
close_selector_lock_copy_for_npm
"${nodejs_npm}" "$@" "${npm_prefix_args[@]}"
npm_status=$?
if [ "${npm_status}" -eq 0 ] && [ -n "${ci_aside}" ]; then
rm -rf -- "${ci_aside}"
ci_aside=''
fi
if [ "${npm_status}" -eq 0 ] && is_npm_ci "${npm_classification_args[@]}"; then
invalidate_shared_node_modules_marker
fi
finish_classic_lockfiles
prepare_status=$?
if [ "${npm_status}" -ne 0 ]; then
exit "${npm_status}"
fi
exit "${prepare_status}"
else
if is_classic_npm_mutation "${npm_classification_args[@]}"; then
bun_unshare_before_classic || exit $?
invalidate_shared_node_modules_marker
fi
# npm invoked from the application root may replace its node_modules
# symlink while updating a now-private delivered tree. Preserve only the
# selector's exact link by making the venv library the npm prefix; a
# user-created real directory (or another link) remains untouched.
if is_classic_npm_mutation "${npm_classification_args[@]}" \
&& [[ -L "${app_node_modules}" \
&& "$(readlink "${app_node_modules}")" == "${venv_node_modules}" ]]; then
close_selector_lock_copy_for_npm
"${nodejs_npm}" "$@" "${npm_prefix_args[@]}"
npm_status=$?
finish_classic_lockfiles
prepare_status=$?
if [ "${npm_status}" -ne 0 ]; then
exit "${npm_status}"
fi
exit "${prepare_status}"
fi
close_selector_lock_copy_for_npm
exec "${nodejs_npm}" "$@"
fi