From 1526b3433e58068e18354f0bc14a70c0d7996ab8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 28 Oct 2026 15:05:04 +0000 Subject: [PATCH 1/2] feat(build): publish a source distribution without Node.js The release publishes a sdist next to the platform wheels. The sdist carries the "any" driver bundle, built from .upstream/ like the others but without the Node.js binary. A wheel built from it is py3-none-any and runs with the Node.js of PLAYWRIGHT_NODEJS_PATH. The client says so when the bundled Node.js is missing, and names the executable when the driver does not launch. --- .github/workflows/ci.yml | 32 ++++++++++++ .github/workflows/publish.yml | 14 ++--- AGENTS.md | 1 + playwright/__main__.py | 20 ++++++-- playwright/_impl/_driver.py | 21 ++++++-- playwright/_impl/transport.py | 31 ++++++----- scripts/build_driver.py | 9 +++- setup.py | 76 ++++++++++++++++++++------- tests/test_installation.py | 97 +++++++++++++++++++++++++++-------- 9 files changed, 232 insertions(+), 69 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 240afe8f5..f50ba4bba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,6 +196,38 @@ jobs: if: ${{ !cancelled() && steps.install.outcome == 'success' }} run: $CI_ENV_EXEC pytest tests/async --browser=chromium --browser-channel=${{ matrix.browser-channel }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes + sdist: + name: Source distribution + # The sdist has the driver without Node.js, for the installs that reject wheels + # (poetry's installer.no-binary) and the platforms without a wheel. It runs with + # the Node.js of PLAYWRIGHT_NODEJS_PATH. + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.10" + - name: Set up Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: '24' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r local-requirements.txt + pip install -r requirements.txt + pip install -e . + - name: Get Playwright + run: python scripts/upstream.py checkout + - name: Build the driver and the sdist + run: python -m build --sdist + - name: Install browser dependencies + run: PLAYWRIGHT_NODEJS_PATH=$(which node) python -m playwright install-deps chromium + - name: Test the sdist installation + run: pytest tests/test_installation.py -k test_install_sdist --browser=chromium + test_examples: name: Examples runs-on: ubuntu-22.04 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 18d139ee1..04c8c4be0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -48,20 +48,22 @@ jobs: run: python scripts/upstream.py checkout # Each wheel build builds the driver of its platform; the build of # .upstream/ is shared. - - name: Build the driver and the wheels + - name: Build the driver, the wheels and the sdist run: | for wheel in $(python setup.py --list-wheels); do PLAYWRIGHT_TARGET_WHEEL=$wheel python -m build --wheel --outdir dist done - - name: Check the wheels + python -m build --sdist --outdir dist + - name: Check the wheels and the sdist run: | ls -l dist test "$(ls dist/*.whl | wc -l)" -eq "$(python setup.py --list-wheels | wc -l)" + test -f "dist/playwright-${TAG#v}.tar.gz" for file in dist/*; do - if [[ "$(basename "$file")" != "playwright-${TAG#v}-"*.whl ]]; then - echo "Unexpected file for $TAG: $file" - exit 1 - fi + case "$(basename "$file")" in + "playwright-${TAG#v}-"*.whl | "playwright-${TAG#v}.tar.gz") ;; + *) echo "Unexpected file for $TAG: $file"; exit 1 ;; + esac done - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: diff --git a/AGENTS.md b/AGENTS.md index bd3a84377..2774ce29f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ pip install -r local-requirements.txt pip install -e . python scripts/upstream.py checkout python -m build --wheel # builds the driver from .upstream/ into playwright/driver/ +python -m build --sdist # the driver without Node.js; runs with PLAYWRIGHT_NODEJS_PATH playwright install chromium # not --with-deps: sudo is denied here ./test.sh [browser] # tests/sync and tests/async, under xvfb ./scripts/lint.sh # the Lint job of CI; run it before you commit diff --git a/playwright/__main__.py b/playwright/__main__.py index b38ae8a95..62bcf09d8 100644 --- a/playwright/__main__.py +++ b/playwright/__main__.py @@ -15,16 +15,28 @@ import subprocess import sys -from playwright._impl._driver import compute_driver_executable, get_driver_env +from playwright._impl._driver import ( + compute_driver_executable, + get_driver_env, + node_launch_error, +) +from playwright._impl._errors import Error def main() -> None: try: driver_executable, driver_cli = compute_driver_executable() - completed_process = subprocess.run( - [driver_executable, driver_cli, *sys.argv[1:]], env=get_driver_env() - ) + try: + completed_process = subprocess.run( + [driver_executable, driver_cli, *sys.argv[1:]], env=get_driver_env() + ) + except OSError as error: + raise node_launch_error(driver_executable, error) sys.exit(completed_process.returncode) + except Error as error: + # No Node.js to run the driver with: a message, not a traceback. + print(error.message, file=sys.stderr) + sys.exit(1) except KeyboardInterrupt: sys.exit(130) diff --git a/playwright/_impl/_driver.py b/playwright/_impl/_driver.py index 22b53b8e7..f26f88cc2 100644 --- a/playwright/_impl/_driver.py +++ b/playwright/_impl/_driver.py @@ -19,18 +19,29 @@ from typing import Tuple import playwright +from playwright._impl._errors import Error from playwright._repo_version import version def compute_driver_executable() -> Tuple[str, str]: driver_path = Path(inspect.getfile(playwright)).parent / "driver" cli_path = str(driver_path / "package" / "cli.js") - if sys.platform == "win32": - return ( - os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node.exe")), - cli_path, + node_path = os.getenv("PLAYWRIGHT_NODEJS_PATH") + if node_path: + return (node_path, cli_path) + bundled_node = driver_path / ("node.exe" if sys.platform == "win32" else "node") + if not bundled_node.is_file(): + # The source distribution ships the driver without Node.js. + raise Error( + "This installation of Playwright does not include Node.js. " + "Set PLAYWRIGHT_NODEJS_PATH pointing to Node.js executable." ) - return (os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node")), cli_path) + return (str(bundled_node), cli_path) + + +def node_launch_error(executable: str, error: OSError) -> Error: + """The error for a spawn of the driver that failed.""" + return Error(f"Cannot launch Node.js executable at {executable}: {error}") def get_driver_env() -> dict: diff --git a/playwright/_impl/transport.py b/playwright/_impl/transport.py index a45196c45..ad249807e 100644 --- a/playwright/_impl/transport.py +++ b/playwright/_impl/transport.py @@ -29,7 +29,11 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Dict, List, Optional, Union -from playwright._impl._driver import compute_driver_executable, get_driver_env +from playwright._impl._driver import ( + compute_driver_executable, + get_driver_env, + node_launch_error, +) from playwright._impl.errors import Error from playwright._impl.utils.tasks import wait_for_next_task @@ -133,17 +137,20 @@ async def connect(self) -> None: startupinfo.wShowWindow = subprocess.SW_HIDE executable_path, entrypoint_path = compute_driver_executable() - self._proc = await asyncio.create_subprocess_exec( - executable_path, - entrypoint_path, - "run-driver", - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=_get_stderr_fileno(), - limit=32768, - env=env, - startupinfo=startupinfo, - ) + try: + self._proc = await asyncio.create_subprocess_exec( + executable_path, + entrypoint_path, + "run-driver", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=_get_stderr_fileno(), + limit=32768, + env=env, + startupinfo=startupinfo, + ) + except OSError as error: + raise node_launch_error(executable_path, error) except Exception as exc: self.on_error_future.set_exception(exc) raise exc diff --git a/scripts/build_driver.py b/scripts/build_driver.py index e5265d59c..bdf171b75 100755 --- a/scripts/build_driver.py +++ b/scripts/build_driver.py @@ -28,6 +28,9 @@ LICENSE - the Node.js license package/** - the playwright-core npm package +The ``any`` bundle, for the source distribution, has only ``package/**``: it runs +with the Node.js of ``PLAYWRIGHT_NODEJS_PATH``. + A bundle that exists is not built again. ``.upstream/`` must be checked out at the commit in ``DRIVER_VERSION`` (``python scripts/upstream.py checkout``), with no local changes. npm uses the registry and the credentials of a root-level ``.npmrc`` when @@ -63,7 +66,7 @@ class Platform(NamedTuple): suffix: str # bundle suffix; matches the "zip_name" values in setup.py - node_dir: str # the nodejs.org archive name infix: node-v- + node_dir: str # the nodejs.org archive name infix: node-v-; "" for no Node.js windows: bool @@ -75,6 +78,7 @@ class Platform(NamedTuple): Platform("linux-arm64", "linux-arm64", False), Platform("win32_x64", "win-x64", True), Platform("win32_arm64", "win-arm64", True), + Platform("any", "", False), # the source distribution: no Node.js ] @@ -240,7 +244,8 @@ def assemble( if bundle_dir.exists(): shutil.rmtree(bundle_dir) bundle_dir.mkdir(parents=True) - fetch_node(platform, node_version, bundle_dir, work_dir) + if platform.node_dir: + fetch_node(platform, node_version, bundle_dir, work_dir) shutil.copytree(package_dir, bundle_dir / "package") destination = DRIVER_DIR / f"playwright-{commit}-{platform.suffix}.zip" zip_bundle(bundle_dir, destination) diff --git a/setup.py b/setup.py index 786df1c4d..c1d589bf0 100644 --- a/setup.py +++ b/setup.py @@ -27,12 +27,22 @@ # in .upstream/, the microsoft/playwright checkout at the commit in DRIVER_VERSION, # and adds the Node.js binary of NODE_VERSION. The bundles are named after the # commit (driver/playwright--.zip), so a roll builds new ones. +# +# A sdist build (python -m build --sdist) builds the "any" bundle the same way, but +# without the Node.js binary, and ships it. A wheel built from the sdist packs that +# bundle, so it is py3-none-any and runs with the Node.js of PLAYWRIGHT_NODEJS_PATH. def read_driver_version() -> str: return (Path(__file__).parent / "DRIVER_VERSION").read_text().strip() driver_version = read_driver_version() +# The sdist unpacks with its metadata at the root; a checkout has none there. +building_from_sdist = (Path(__file__).parent / "PKG-INFO").is_file() + +# "wheel" is the platform tag of the wheel name, as in base_wheel_bundles. +sdist_bundle = {"wheel": "any.whl", "zip_name": "any"} + base_wheel_bundles = [ { "wheel": "macosx_10_13_x86_64.whl", @@ -90,6 +100,7 @@ def read_driver_version() -> str: exit(0) from setuptools import setup # noqa: E402 +from setuptools.command.sdist import sdist as SDistCommand # noqa: E402 try: from auditwheel.wheeltools import InWheel @@ -107,10 +118,18 @@ def extractall(zip: zipfile.ZipFile, path: str) -> None: os.chmod(extracted_path, attr) +def driver_bundle_path(zip_name: str) -> str: + return f"driver/playwright-{driver_version}-{zip_name}.zip" + + def ensure_driver_bundle(zip_name: str) -> None: - destination_path = f"driver/playwright-{driver_version}-{zip_name}.zip" + destination_path = driver_bundle_path(zip_name) if os.path.exists(destination_path): return + if building_from_sdist: + raise RuntimeError( + f"Driver bundle {destination_path} is missing from the source distribution." + ) # Build only this platform's bundle, so that a wheel build downloads just the # one Node.js binary it needs. build_script = os.path.join(os.path.dirname(__file__), "scripts", "build_driver.py") @@ -121,6 +140,16 @@ def ensure_driver_bundle(zip_name: str) -> None: ) +class PlaywrightSDistCommand(SDistCommand): + def make_distribution(self) -> None: + # The driver without Node.js, built from .upstream/ like the platform + # bundles; the wheel build of the sdist packs it. The file list comes + # from egg_info (the files that git tracks), so the bundle is added here. + ensure_driver_bundle(sdist_bundle["zip_name"]) + self.filelist.append(driver_bundle_path(sdist_bundle["zip_name"])) + super().make_distribution() + + class PlaywrightBDistWheelCommand(BDistWheelCommand): def run(self) -> None: # setuptools copies the package into build/lib and keeps what is there: a @@ -132,7 +161,9 @@ def run(self) -> None: self._download_and_extract_local_driver() wheel = None - if os.getenv("PLAYWRIGHT_TARGET_WHEEL", None): + if building_from_sdist: + wheel = sdist_bundle + elif os.getenv("PLAYWRIGHT_TARGET_WHEEL", None): wheel = list( filter( lambda wheel: wheel["wheel"] @@ -162,14 +193,16 @@ def _build_wheel( # Although the build produces every platform's bundle, only this wheel's # target platform driver is extracted and packed below, so the wheel # stays single-platform. - zip_file = f"driver/playwright-{driver_version}-{wheel_bundle['zip_name']}.zip" + zip_file = driver_bundle_path(wheel_bundle["zip_name"]) extract_dir = f"driver/{wheel_bundle['zip_name']}" if os.path.exists(extract_dir): shutil.rmtree(extract_dir) with zipfile.ZipFile(zip_file, "r") as zip: extractall(zip, extract_dir) wheel_location = without_platform + wheel_bundle["wheel"] - shutil.copy(base_wheel_location, wheel_location) + # The base wheel is py3-none-any already: the "any" wheel adds to it in place. + if wheel_location != base_wheel_location: + shutil.copy(base_wheel_location, wheel_location) with zipfile.ZipFile( wheel_location, mode="a", compression=zipfile.ZIP_DEFLATED ) as zip: @@ -183,7 +216,8 @@ def _build_wheel( "playwright/driver/README.md", f"{wheel_bundle['wheel']} driver package", ) - os.remove(base_wheel_location) + if wheel_location != base_wheel_location: + os.remove(base_wheel_location) for whlfile in glob.glob(os.path.join(self.dist_dir, "*.whl")): os.makedirs("wheelhouse", exist_ok=True) if InWheel: @@ -197,20 +231,23 @@ def _build_wheel( def _download_and_extract_local_driver( self, ) -> None: - zip_names_for_current_system = set( - map( - lambda wheel: wheel["zip_name"], - filter( - lambda wheel: wheel["machine"] == platform.machine().lower() - and wheel["platform"] == sys.platform, - base_wheel_bundles, - ), + if building_from_sdist: + zip_name = sdist_bundle["zip_name"] + else: + zip_names_for_current_system = set( + map( + lambda wheel: wheel["zip_name"], + filter( + lambda wheel: wheel["machine"] == platform.machine().lower() + and wheel["platform"] == sys.platform, + base_wheel_bundles, + ), + ) ) - ) - assert len(zip_names_for_current_system) == 1 - zip_name = zip_names_for_current_system.pop() + assert len(zip_names_for_current_system) == 1 + zip_name = zip_names_for_current_system.pop() ensure_driver_bundle(zip_name) - zip_file = f"driver/playwright-{driver_version}-{zip_name}.zip" + zip_file = driver_bundle_path(zip_name) if os.path.exists("playwright/driver"): shutil.rmtree("playwright/driver") with zipfile.ZipFile(zip_file, "r") as zip: @@ -218,5 +255,8 @@ def _download_and_extract_local_driver( setup( - cmdclass={"bdist_wheel": PlaywrightBDistWheelCommand}, + cmdclass={ + "sdist": PlaywrightSDistCommand, + "bdist_wheel": PlaywrightBDistWheelCommand, + }, ) diff --git a/tests/test_installation.py b/tests/test_installation.py index 85d803d12..5a4f65b27 100644 --- a/tests/test_installation.py +++ b/tests/test_installation.py @@ -21,38 +21,91 @@ import pytest +ROOT = Path(__file__).parent.parent.resolve() + + +def _create_venv(env_dir: Path) -> str: + env = EnvBuilder(with_pip=True) + env.create(env_dir=env_dir) + return env.ensure_directories(env_dir).env_exe + + +def _run_client(python: str, tmp_path: Path, browser_name: str, environ: dict) -> None: + subprocess.check_output( + [python, "-m", "playwright", "install", browser_name], env=environ + ) + shutil.copyfile(ROOT / "tests" / "assets" / "client.py", tmp_path / "main.py") + subprocess.check_output( + [python, str(tmp_path / "main.py"), browser_name], env=environ + ) + assert (tmp_path / f"{browser_name}.png").exists() + # A venv, a pip install of the wheel and a browser download: minutes, not the 30 s # of a test. @pytest.mark.timeout(900) def test_install(tmp_path: Path, browser_name: str) -> None: - env_dir = tmp_path / "env" - env = EnvBuilder(with_pip=True) - env.create(env_dir=env_dir) - context = env.ensure_directories(env_dir) - root = Path(__file__).parent.parent.resolve() + python = _create_venv(tmp_path / "env") if sys.platform == "win32": - wheelpath = list((root / "dist").glob("playwright*win_amd64*.whl"))[0] + wheelpath = list((ROOT / "dist").glob("playwright*win_amd64*.whl"))[0] elif sys.platform == "linux": - wheelpath = list((root / "dist").glob("playwright*manylinux1*.whl"))[0] + wheelpath = list((ROOT / "dist").glob("playwright*manylinux1*.whl"))[0] elif sys.platform == "darwin": - wheelpath = list((root / "dist").glob("playwright*macosx_*.whl"))[0] - subprocess.check_output( - [ - context.env_exe, - "-m", - "pip", - "install", - str(wheelpath), - ] + wheelpath = list((ROOT / "dist").glob("playwright*macosx_*.whl"))[0] + subprocess.check_output([python, "-m", "pip", "install", str(wheelpath)]) + environ = os.environ.copy() + environ["PLAYWRIGHT_BROWSERS_PATH"] = str(tmp_path) + _run_client(python, tmp_path, browser_name, environ) + + +# The sdist (python -m build --sdist) has the driver without Node.js: it runs with +# the Node.js of PLAYWRIGHT_NODEJS_PATH, and says so when the variable is missing. +@pytest.mark.timeout(900) +def test_install_sdist(tmp_path: Path, browser_name: str) -> None: + node = shutil.which("node") + assert node, "node must be in PATH" + python = _create_venv(tmp_path / "env") + sdist = list((ROOT / "dist").glob("playwright-*.tar.gz"))[0] + subprocess.check_output([python, "-m", "pip", "install", str(sdist)]) + driver_dir = Path( + subprocess.check_output( + [ + python, + "-c", + "import playwright, pathlib; print(pathlib.Path(playwright.__file__).parent / 'driver')", + ], + cwd=tmp_path, + text=True, + ).strip() ) + assert (driver_dir / "package" / "cli.js").exists() + assert not (driver_dir / "node").exists() + assert not (driver_dir / "node.exe").exists() + environ = os.environ.copy() environ["PLAYWRIGHT_BROWSERS_PATH"] = str(tmp_path) - subprocess.check_output( - [context.env_exe, "-m", "playwright", "install", browser_name], env=environ + environ.pop("PLAYWRIGHT_NODEJS_PATH", None) + result = subprocess.run( + [python, "-m", "playwright", "--version"], + cwd=tmp_path, + env=environ, + capture_output=True, + text=True, ) - shutil.copyfile(root / "tests" / "assets" / "client.py", tmp_path / "main.py") - subprocess.check_output( - [context.env_exe, str(tmp_path / "main.py"), browser_name], env=environ + assert result.returncode == 1 + assert "Set PLAYWRIGHT_NODEJS_PATH" in result.stderr + + environ["PLAYWRIGHT_NODEJS_PATH"] = str(tmp_path / "no-such-node") + result = subprocess.run( + [python, "-m", "playwright", "--version"], + cwd=tmp_path, + env=environ, + capture_output=True, + text=True, ) - assert (tmp_path / f"{browser_name}.png").exists() + assert result.returncode == 1 + assert "Cannot launch Node.js executable at" in result.stderr + assert "no-such-node" in result.stderr + + environ["PLAYWRIGHT_NODEJS_PATH"] = node + _run_client(python, tmp_path, browser_name, environ) From ab5bcf4167e36d43aea1f53feba0f3d2190c78c3 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 28 Oct 2026 20:53:04 +0000 Subject: [PATCH 2/2] test(build): fix the installation tests of the sdist - The wheel jobs run test_install only; the sdist job runs test_install_sdist. Each job builds the one artifact its test installs. - The sdist job builds the wheel first, so that the checkout has a driver with Node.js for install-deps. - The client subprocesses of the tests run from tmp_path, so that they import the installed package and not the checkout. - The sdist test installs with --no-binary :all:, the pip form of poetry's installer.no-binary policy of the report. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 12 ++++++++---- tests/test_installation.py | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f50ba4bba..372dc6654 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,7 +135,7 @@ jobs: - name: Test Reference count run: $CI_ENV_EXEC pytest tests/test_reference_count_async.py --browser=${{ matrix.browser }} - name: Test Wheel Installation - run: $CI_ENV_EXEC pytest tests/test_installation.py --browser=${{ matrix.browser }} + run: $CI_ENV_EXEC pytest tests/test_installation.py::test_install --browser=${{ matrix.browser }} - name: Test Sync API run: $CI_ENV_EXEC pytest tests/sync --browser=${{ matrix.browser }} -n ${{ env.WORKERS }} -p pytest_dot_reporter -rfE --color=yes - name: Test Async API @@ -221,12 +221,16 @@ jobs: pip install -e . - name: Get Playwright run: python scripts/upstream.py checkout - - name: Build the driver and the sdist + # The wheel build puts the driver with Node.js into the checkout, for the + # install-deps step below. The test installs the sdist in its own venv. + - name: Build the driver and the wheel + run: python -m build --wheel + - name: Build the sdist run: python -m build --sdist - name: Install browser dependencies - run: PLAYWRIGHT_NODEJS_PATH=$(which node) python -m playwright install-deps chromium + run: python -m playwright install-deps chromium - name: Test the sdist installation - run: pytest tests/test_installation.py -k test_install_sdist --browser=chromium + run: pytest tests/test_installation.py::test_install_sdist --browser=chromium test_examples: name: Examples diff --git a/tests/test_installation.py b/tests/test_installation.py index 5a4f65b27..8ec8eb31d 100644 --- a/tests/test_installation.py +++ b/tests/test_installation.py @@ -31,12 +31,16 @@ def _create_venv(env_dir: Path) -> str: def _run_client(python: str, tmp_path: Path, browser_name: str, environ: dict) -> None: + # cwd=tmp_path: from the repository root, "-m playwright" imports the checkout + # instead of the installed package. subprocess.check_output( - [python, "-m", "playwright", "install", browser_name], env=environ + [python, "-m", "playwright", "install", browser_name], + cwd=tmp_path, + env=environ, ) shutil.copyfile(ROOT / "tests" / "assets" / "client.py", tmp_path / "main.py") subprocess.check_output( - [python, str(tmp_path / "main.py"), browser_name], env=environ + [python, str(tmp_path / "main.py"), browser_name], cwd=tmp_path, env=environ ) assert (tmp_path / f"{browser_name}.png").exists() @@ -66,7 +70,12 @@ def test_install_sdist(tmp_path: Path, browser_name: str) -> None: assert node, "node must be in PATH" python = _create_venv(tmp_path / "env") sdist = list((ROOT / "dist").glob("playwright-*.tar.gz"))[0] - subprocess.check_output([python, "-m", "pip", "install", str(sdist)]) + # No wheels at all, as poetry's installer.no-binary=:all: in the report: the + # dependencies come from source too. The isolated build environment still + # takes wheels: pip does not pass the policy on to it, and neither does poetry. + subprocess.check_output( + [python, "-m", "pip", "install", "--no-binary", ":all:", str(sdist)] + ) driver_dir = Path( subprocess.check_output( [