diff --git a/src/playwright/NOTES.md b/src/playwright/NOTES.md index 56c612e..6426f01 100644 --- a/src/playwright/NOTES.md +++ b/src/playwright/NOTES.md @@ -13,6 +13,28 @@ when it cannot find it — it never installs node for you, so your own version p Debian and Ubuntu only. Playwright installs Chromium's shared libraries with apt and has no equivalent for other package managers. +## Node versions + +The CLI is installed under its own prefix, `/usr/local/share/playwright-cli`, and symlinked to +`/usr/local/bin/playwright-cli`. It is deliberately not a plain `npm install --global`: the node +feature installs node through nvm, whose global root is one directory per node version +(`$NVM_DIR/versions/node//lib/node_modules`), with `$NVM_DIR/current/bin` on `PATH`. A +`npm install --global` there belongs to whichever version was active at build time, so `nvm install` +or `nvm use` repoints `current` and `playwright-cli` drops off `PATH` for the whole container — +not just the shell that switched. + +Switch node freely; one copy of the CLI stays reachable, and it runs on whatever node is active +(the package needs node 18 or newer). + +The install stays root-owned and read-only to the remote user. One copy now backs every node +version, so no single user should be able to rewrite what the others execute. + +It runs from environments that inherit `containerEnv`, which is how the CLI finds both node and its +browsers. `sudo` is not one of them — it replaces `PATH` with `secure_path` and drops `containerEnv`, +and `sudo playwright-cli` has never worked for this feature. Making it work would mean root +executing an interpreter and a browser out of trees the remote user owns, so it stays unsupported; +run the CLI as the remote user. + ## Browsers Chromium only, plus the headless shell and ffmpeg that come with it. Firefox and WebKit are not diff --git a/src/playwright/README.md b/src/playwright/README.md index 40702ca..1f40b00 100644 --- a/src/playwright/README.md +++ b/src/playwright/README.md @@ -32,6 +32,28 @@ when it cannot find it — it never installs node for you, so your own version p Debian and Ubuntu only. Playwright installs Chromium's shared libraries with apt and has no equivalent for other package managers. +## Node versions + +The CLI is installed under its own prefix, `/usr/local/share/playwright-cli`, and symlinked to +`/usr/local/bin/playwright-cli`. It is deliberately not a plain `npm install --global`: the node +feature installs node through nvm, whose global root is one directory per node version +(`$NVM_DIR/versions/node//lib/node_modules`), with `$NVM_DIR/current/bin` on `PATH`. A +`npm install --global` there belongs to whichever version was active at build time, so `nvm install` +or `nvm use` repoints `current` and `playwright-cli` drops off `PATH` for the whole container — +not just the shell that switched. + +Switch node freely; one copy of the CLI stays reachable, and it runs on whatever node is active +(the package needs node 18 or newer). + +The install stays root-owned and read-only to the remote user. One copy now backs every node +version, so no single user should be able to rewrite what the others execute. + +It runs from environments that inherit `containerEnv`, which is how the CLI finds both node and its +browsers. `sudo` is not one of them — it replaces `PATH` with `secure_path` and drops `containerEnv`, +and `sudo playwright-cli` has never worked for this feature. Making it work would mean root +executing an interpreter and a browser out of trees the remote user owns, so it stays unsupported; +run the CLI as the remote user. + ## Browsers Chromium only, plus the headless shell and ffmpeg that come with it. Firefox and WebKit are not diff --git a/src/playwright/devcontainer-feature.json b/src/playwright/devcontainer-feature.json index 61f3091..8ce9019 100644 --- a/src/playwright/devcontainer-feature.json +++ b/src/playwright/devcontainer-feature.json @@ -17,5 +17,5 @@ "type": "string" } }, - "version": "1.1.0" + "version": "1.2.0" } diff --git a/src/playwright/install.sh b/src/playwright/install.sh index 4548c6e..e30801f 100755 --- a/src/playwright/install.sh +++ b/src/playwright/install.sh @@ -3,6 +3,7 @@ set -euo pipefail VERSION="${VERSION:-latest}" BROWSERS_PATH="/usr/local/share/ms-playwright" +INSTALL_PATH="/usr/local/share/playwright-cli" export DEBIAN_FRONTEND=noninteractive export PLAYWRIGHT_BROWSERS_PATH="${BROWSERS_PATH}" @@ -24,15 +25,15 @@ if ! command -v npm >/dev/null 2>&1; then exit 1 fi -npm_root="$(npm root -g)" -owner="$(stat -c '%u:%g' "${npm_root}")" +owner="$(stat -c '%u:%g' "$(npm root -g)")" -npm install --global "@playwright/cli@${VERSION}" +npm install --global --prefix "${INSTALL_PATH}" "@playwright/cli@${VERSION}" -playwright_core="$(find "${npm_root}" -maxdepth 6 -path '*/playwright-core/cli.js' -print -quit)" +node_modules="${INSTALL_PATH}/lib/node_modules" +playwright_core="$(find "${node_modules}" -maxdepth 6 -path '*/playwright-core/cli.js' -print -quit)" if [ -z "${playwright_core}" ]; then - echo "playwright-core was not found under ${npm_root}" >&2 + echo "playwright-core was not found under ${node_modules}" >&2 exit 1 fi @@ -44,5 +45,10 @@ node "${playwright_core}" install chromium rm -rf /var/lib/apt/lists/* npm cache clean --force -chown -R "${owner}" "${npm_root}" "$(npm prefix -g)/bin" "${BROWSERS_PATH}" +ln -sfn "${INSTALL_PATH}/bin/playwright-cli" /usr/local/bin/playwright-cli + +chown -R root:root "${INSTALL_PATH}" +chmod -R a+rX,go-w "${INSTALL_PATH}" + +chown -R "${owner}" "${BROWSERS_PATH}" chmod -R a+rX "${BROWSERS_PATH}" diff --git a/test/playwright/scenarios.json b/test/playwright/scenarios.json index 423932e..75b9665 100644 --- a/test/playwright/scenarios.json +++ b/test/playwright/scenarios.json @@ -2,7 +2,7 @@ "ubuntu": { "features": { "ghcr.io/devcontainers/features/common-utils:2": {}, - "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/node:2": {}, "playwright": { "version": "latest" } diff --git a/test/playwright/ubuntu.sh b/test/playwright/ubuntu.sh index 7193cc6..728d9ff 100755 --- a/test/playwright/ubuntu.sh +++ b/test/playwright/ubuntu.sh @@ -4,10 +4,37 @@ set -e # shellcheck source=/dev/null source \ dev-container-features-test-lib + +NVM_DIR="${NVM_DIR:-/usr/local/share/nvm}" + +outside_nvm() { + case "$(readlink -f "$(command -v playwright-cli)")" in + "${NVM_DIR}"/*) return 1 ;; + esac +} + +survives_node_switch() { + # shellcheck source=/dev/null + . "${NVM_DIR}/nvm.sh" && + nvm install 20 >/dev/null && + nvm use 20 >/dev/null && + test "$(node -v | cut -d. -f1)" = v20 && + playwright-cli --version +} + +not_writable_by_remote_user() { + ! test -w /usr/local/bin/playwright-cli && + ! test -w /usr/local/share/playwright-cli/bin/playwright-cli +} + check 'check if playwright-cli exists' bash -c "command -v playwright-cli" check 'check if the browsers path is exported' bash -c "test \"${PLAYWRIGHT_BROWSERS_PATH:-}\" = /usr/local/share/ms-playwright" check 'check if the default browser is chromium' bash -c "test \"${PLAYWRIGHT_MCP_BROWSER:-}\" = chromium" check 'check if chromium was downloaded' bash -c "ls /usr/local/share/ms-playwright | grep -q chromium" check 'check if the browsers are readable by the remote user' bash -c "test -r /usr/local/share/ms-playwright && test -x /usr/local/share/ms-playwright" check 'check if the browser opens with no --browser flag' bash -c "PLAYWRIGHT_CLI_SESSION=featuretest playwright-cli open && PLAYWRIGHT_CLI_SESSION=featuretest playwright-cli close" +check 'check if playwright-cli lives outside the nvm version directories' outside_nvm +check 'check if the shared install is read-only to the remote user' not_writable_by_remote_user +# Keep last: repoints nvm's "current" symlink for every later check. +check 'check if playwright-cli survives a node version switch' survives_node_switch reportResults