Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions .azure-pipelines/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,55 @@ extends:
path: $(Build.ArtifactStagingDirectory)/esrp-build
artifact: esrp-build
steps:
- bash: |
if [[ ! "$CURRENT_BRANCH" =~ ^v1\..* ]]; then
echo "Can only publish from a release tag (v1.*)."
echo "Unexpected ref name: $CURRENT_BRANCH"
exit 1
fi
env:
CURRENT_BRANCH: ${{ variables['Build.SourceBranchName'] }}
displayName: 'Check the ref is a release tag'
# Allow manual runs on any branch to exercise the build without publishing.
condition: ne(variables['Build.Reason'], 'Manual')
- task: UsePythonVersion@0
inputs:
versionSpec: '3.10'
displayName: 'Use Python'
- task: NodeTool@0
# Resolve every pip install (including the isolated build environments
# that `python -m build` and `pip install -e .` create) through the
# DevDiv_PublicPackages Azure Artifacts feed instead of pypi.org, as
# required by SFI-ES4.2.4. The task exports an authenticated PIP_INDEX_URL.
- task: PipAuthenticate@1
inputs:
versionSpec: '24.x'
displayName: 'Use Node.js'
artifactFeeds: DevDiv/DevDiv_PublicPackages
displayName: 'Authenticate pip to DevDiv_PublicPackages feed'
- task: UseNode@1
inputs:
version: '24.x'
displayName: 'Install Node.js'
# scripts/build_driver.py fetches playwright-core with `npm pack`, which
# picks up the registry and credentials from this .npmrc.
- script: echo "registry=https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/DevDiv_PublicPackages/npm/registry/" >> .npmrc
displayName: 'Point npm registry at DevDiv_PublicPackages feed'
- task: npmAuthenticate@0
inputs:
workingFile: .npmrc
displayName: 'Authenticate npm to DevDiv_PublicPackages feed'
- script: |
python -m pip install --upgrade pip
pip install -r local-requirements.txt
pip install -r requirements.txt
pip install -e .
python -m pip install --upgrade pip --disable-pip-version-check
pip install -r local-requirements.txt --disable-pip-version-check
pip install -r requirements.txt --disable-pip-version-check
pip install -e . --disable-pip-version-check
for wheel in $(python setup.py --list-wheels); do
PLAYWRIGHT_TARGET_WHEEL=$wheel python -m build --wheel --outdir $(Build.ArtifactStagingDirectory)/esrp-build
done
displayName: 'Install & Build'
- job: Publish
dependsOn: Build
# Only publish from release tags; manual runs on a branch stop after Build,
# which lets the build be exercised without publishing.
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v1.'))
templateContext:
type: releaseJob
isProduction: true
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ utils/docker/dist/
Pipfile
Pipfile.lock
.venv/

# Written by the release pipeline (npmAuthenticate@0); holds feed credentials.
.npmrc
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk
- `tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror.
- `DRIVER_VERSION` — the single source of truth for which Playwright release the driver is assembled from (one line, the `playwright-core` npm version, e.g. `1.61.0`, no `v` prefix). Read by `setup.py`, `scripts/build_driver.py`, and CI. The wheel build downloads `playwright-core` at this version from npm plus the matching Node.js binary and assembles the per-platform bundles — no source build. The version is baked into the staged bundle filenames (`driver/playwright-<version>-<suffix>.zip`), so it doubles as the build cache key.
- `NODE_VERSION` — the Node.js version bundled with the driver (one line, e.g. `24.16.0`). Maintained at roll time by `scripts/update_node_version.py` (latest LTS, mirroring upstream's `utils/build/update-playwright-node.mjs`).
- `scripts/build_driver.py` — assembles the per-platform driver bundles into `driver/` by downloading the `playwright-core` npm package (`DRIVER_VERSION`) and the official Node.js binaries (`NODE_VERSION`). Pure Python stdlib (no Node/npm/git); invoked from `setup.py`'s `bdist_wheel` with the target platform's suffix (no arg builds all six).
- `scripts/build_driver.py` — assembles the per-platform driver bundles into `driver/` by downloading the `playwright-core` npm package (`DRIVER_VERSION`) and the official Node.js binaries (`NODE_VERSION`). Fetches `playwright-core` with `npm pack` (needs Node.js/npm on PATH; honours a root `.npmrc`) and the Node.js binaries over plain HTTP; invoked from `setup.py`'s `bdist_wheel` with the target platform's suffix (no arg builds all six).
- `api.json` is **not** shipped in the bundle and is never written into the driver — `scripts/update_api.sh` generates it from a nearby `microsoft/playwright` checkout (`$PW_SRC_DIR`) into a temp file and passes it to codegen via `PW_API_JSON` (read by `scripts/documentation_provider.py`). Needed only when regenerating the API, never at runtime.
- `ROLLING.md`, `CONTRIBUTING.md` — human-facing setup and roll docs.

## Setup

`CONTRIBUTING.md` has the full sequence. The short version (needs Node.js, npm, git and bash for the driver build):
`CONTRIBUTING.md` has the full sequence. The short version (needs Node.js and npm for the driver build):

```sh
python3 -m venv env && source env/bin/activate
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Build and install drivers:

The driver is assembled from published artifacts — the `playwright-core` npm
package (version pinned in `DRIVER_VERSION`) and the official Node.js binary
(pinned in `NODE_VERSION`). Building a wheel just downloads them; no Node/npm/git
toolchain is required.
(pinned in `NODE_VERSION`). Building a wheel downloads them with `npm pack` and
plain HTTP, so Node.js/npm must be installed; no git or source build is needed.

```sh
pip install -e .
Expand Down
2 changes: 1 addition & 1 deletion ROLLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pre-commit install
pip install -e .
```
* change the driver pin in `DRIVER_VERSION` (the `playwright-core` npm version, e.g. `1.61.0`) and refresh `NODE_VERSION`: `python scripts/update_node_version.py`
* download the new driver: `python -m build --wheel` (fetches `playwright-core` from npm + the matching Node.js binary and assembles the bundle; no source build). Set `npm_config_registry` to use a different npm registry.
* download the new driver: `python -m build --wheel` (fetches `playwright-core` via `npm pack` + the matching Node.js binary and assembles the bundle; no source build).
* generate API (needs a nearby `microsoft/playwright` checkout at `v<new>`): `PW_SRC_DIR=../playwright ./scripts/update_api.sh`
* commit changes & send PR
* wait for bots to pass & merge the PR
Expand Down
36 changes: 25 additions & 11 deletions scripts/build_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,24 @@
LICENSE - the Node.js license
package/** - the playwright-core npm package

Unlike the old source build this needs no Node.js, npm, git or bash — only the
Python standard library.
``playwright-core`` is fetched with ``npm pack`` (run from the repository root, so
a root-level ``.npmrc`` -- e.g. the one the release pipeline writes to point at
the internal Azure Artifacts feed -- and its credentials are honoured). The
Node.js binaries are downloaded directly from nodejs.org. Apart from Node.js/npm
this needs only the Python standard library.

Usage::

scripts/build_driver.py # assemble every platform bundle
scripts/build_driver.py <suffix> # assemble a single bundle, e.g. mac-arm64

Set ``npm_config_registry`` to download ``playwright-core`` from an
alternative npm registry.

``setup.py`` invokes the single-suffix form so a wheel build only downloads the
one Node.js binary it needs.
"""

import os
import shutil
import subprocess
import sys
import tarfile
import tempfile
Expand All @@ -56,9 +57,6 @@
REPO_ROOT = Path(__file__).resolve().parent.parent
DRIVER_DIR = REPO_ROOT / "driver"

NPM_REGISTRY = os.environ.get(
"npm_config_registry", "https://registry.npmjs.org"
).rstrip("/")
NODEJS_DIST = "https://nodejs.org/dist"


Expand Down Expand Up @@ -136,9 +134,25 @@ def _extract_zip_file(archive: zipfile.ZipFile, name: str, destination: Path) ->

def fetch_playwright_core(version: str, work_dir: Path) -> Path:
"""Download playwright-core@<version> and extract its package/ tree once."""
url = f"{NPM_REGISTRY}/playwright-core/-/playwright-core-{version}.tgz"
npm = "npm.cmd" if sys.platform == "win32" else "npm"
spec = f"playwright-core@{version}"
# npm is run from the repository root so that a root-level .npmrc (registry
# and credentials) is honoured. `npm pack` writes <name>-<version>.tgz.
print(f"Downloading {spec} with npm pack", flush=True)
try:
subprocess.check_call(
[npm, "pack", spec, "--pack-destination", str(work_dir)],
cwd=REPO_ROOT,
)
except FileNotFoundError:
raise SystemExit(
"npm was not found on PATH; Node.js/npm are required to assemble the driver."
)
except subprocess.CalledProcessError as error:
raise SystemExit(f"npm pack {spec} failed with exit code {error.returncode}")
tgz = work_dir / f"playwright-core-{version}.tgz"
download(url, tgz)
if not tgz.is_file():
raise SystemExit(f"npm pack did not produce {tgz}")
with tarfile.open(tgz, "r:gz") as tar:
# npm tarballs nest every file under a top-level "package/" directory,
# which is exactly the bundle layout we want.
Expand All @@ -148,7 +162,7 @@ def fetch_playwright_core(version: str, work_dir: Path) -> Path:
if m.name == "package" or m.name.startswith("package/")
]
if not members:
raise SystemExit(f"No package/ entries found in {url}")
raise SystemExit(f"No package/ entries found in {tgz.name}")
_extract_members(tar, work_dir, members)
tgz.unlink()
return work_dir / "package"
Expand Down