From 5df6920898398bee53ad20c3e1abeb174c47d979 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 19 Jun 2026 13:56:59 -0700 Subject: [PATCH] feat(docker): pre-extract the driver in images to avoid /tmp unpacking Add a CLI `install-driver` command that extracts the driver (the playwright-core package and the host Node.js binary) into a fixed directory, and a PLAYWRIGHT_DRIVER_DIR environment variable that points Playwright at a preinstalled driver directory (PreinstalledDriver, no extraction). The official Docker images now extract the driver once at build time and select it at runtime, so the library no longer unpacks executables into /tmp on every launch. Fixes: https://github.com/microsoft/playwright-java/issues/1268 --- .../playwright/impl/driver/Driver.java | 10 +++-- .../playwright/impl/driver/jar/DriverJar.java | 44 ++++++++++++++----- .../java/com/microsoft/playwright/CLI.java | 24 +++++++++- .../impl/driver/jar/TestInstall.java | 24 ++++++++++ utils/docker/Dockerfile.jammy | 9 +++- utils/docker/Dockerfile.noble | 9 +++- utils/docker/Dockerfile.resolute | 9 +++- 7 files changed, 112 insertions(+), 17 deletions(-) diff --git a/driver/src/main/java/com/microsoft/playwright/impl/driver/Driver.java b/driver/src/main/java/com/microsoft/playwright/impl/driver/Driver.java index 9b71b2cbc..517650dea 100644 --- a/driver/src/main/java/com/microsoft/playwright/impl/driver/Driver.java +++ b/driver/src/main/java/com/microsoft/playwright/impl/driver/Driver.java @@ -32,6 +32,7 @@ public abstract class Driver { protected final Map env = new LinkedHashMap<>(System.getenv()); public static final String PLAYWRIGHT_NODEJS_PATH = "PLAYWRIGHT_NODEJS_PATH"; + public static final String PLAYWRIGHT_DRIVER_DIR = "PLAYWRIGHT_DRIVER_DIR"; private static Driver instance; @@ -108,9 +109,12 @@ public static Driver createAndInstall(Map env, Boolean installBr } private static Driver newInstance() throws Exception { - String pathFromProperty = System.getProperty("playwright.cli.dir"); - if (pathFromProperty != null) { - return new PreinstalledDriver(Paths.get(pathFromProperty)); + String driverDir = System.getProperty("playwright.cli.dir"); + if (driverDir == null) { + driverDir = System.getenv(PLAYWRIGHT_DRIVER_DIR); + } + if (driverDir != null) { + return new PreinstalledDriver(Paths.get(driverDir)); } String driverImpl = diff --git a/driver/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java b/driver/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java index 060bd9c2d..4e35d21c5 100644 --- a/driver/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java +++ b/driver/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java @@ -30,17 +30,11 @@ public class DriverJar extends Driver { private static final String PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD"; private static final String SELENIUM_REMOTE_URL = "SELENIUM_REMOTE_URL"; private final Path driverTempDir; + private final boolean deleteOnExit; private Path preinstalledNodePath; public DriverJar() throws IOException { - // Allow specifying custom path for the driver installation - // See https://github.com/microsoft/playwright-java/issues/728 - String alternativeTmpdir = System.getProperty("playwright.driver.tmpdir"); - String prefix = "playwright-java-"; - driverTempDir = alternativeTmpdir == null - ? Files.createTempDirectory(prefix) - : Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix); - driverTempDir.toFile().deleteOnExit(); + this(createTempDriverDir(), true); String nodePath = System.getProperty("playwright.nodejs.path"); if (nodePath != null) { preinstalledNodePath = Paths.get(nodePath); @@ -51,6 +45,32 @@ public DriverJar() throws IOException { logMessage("created DriverJar: " + driverTempDir); } + private DriverJar(Path driverDir, boolean deleteOnExit) { + this.driverTempDir = driverDir; + this.deleteOnExit = deleteOnExit; + if (deleteOnExit) { + driverTempDir.toFile().deleteOnExit(); + } + } + + private static Path createTempDriverDir() throws IOException { + // Allow specifying custom path for the driver installation + // See https://github.com/microsoft/playwright-java/issues/728 + String alternativeTmpdir = System.getProperty("playwright.driver.tmpdir"); + String prefix = "playwright-java-"; + return alternativeTmpdir == null + ? Files.createTempDirectory(prefix) + : Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix); + } + + // Extracts the driver (playwright-core package and the Node.js binary for the current platform) + // into the given directory, persistently. Point playwright.cli.dir / PLAYWRIGHT_DRIVER_DIR at it + // to run without extracting to a temp directory on every launch. See issue #1268. + public static void installDriverTo(Path driverDir) throws IOException, URISyntaxException { + Files.createDirectories(driverDir); + new DriverJar(driverDir, false).extractDriverToTempDir(); + } + @Override protected void initialize(Boolean installBrowsers) throws Exception { if (preinstalledNodePath == null && env.containsKey(PLAYWRIGHT_NODEJS_PATH)) { @@ -156,7 +176,9 @@ private void extractResourceToDir(String resourcePath, Path destDir) throws URIS toPath.toFile().setExecutable(true, true); } } - toPath.toFile().deleteOnExit(); + if (deleteOnExit) { + toPath.toFile().deleteOnExit(); + } } catch (IOException e) { throw new RuntimeException("Failed to extract driver from " + uri + ", full uri: " + originalUri, e); } @@ -179,7 +201,9 @@ private URI maybeExtractNestedJar(final URI uri) throws URISyntaxException { Path fromPath = Paths.get(jarUri); Path toPath = driverTempDir.resolve(fromPath.getFileName().toString()); Files.copy(fromPath, toPath); - toPath.toFile().deleteOnExit(); + if (deleteOnExit) { + toPath.toFile().deleteOnExit(); + } return new URI("jar:" + toPath.toUri() + JAR_URL_SEPARATOR + parts[2]); } catch (IOException e) { throw new RuntimeException("Failed to extract driver's nested .jar from " + jarUri + "; full uri: " + uri, e); diff --git a/playwright/src/main/java/com/microsoft/playwright/CLI.java b/playwright/src/main/java/com/microsoft/playwright/CLI.java index 21951bd04..b44febf0b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/CLI.java +++ b/playwright/src/main/java/com/microsoft/playwright/CLI.java @@ -17,9 +17,12 @@ package com.microsoft.playwright; import com.microsoft.playwright.impl.driver.Driver; +import com.microsoft.playwright.impl.driver.jar.DriverJar; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import static java.util.Arrays.asList; @@ -28,7 +31,13 @@ * Use this class to launch playwright cli. */ public class CLI { - public static void main(String[] args) throws IOException, InterruptedException { + public static void main(String[] args) throws IOException, InterruptedException, URISyntaxException { + // Extract the driver into a fixed directory instead of running the playwright CLI. This is + // handled in Java because it must not require an already-extracted driver. See issue #1268. + if (args.length > 0 && "install-driver".equals(args[0])) { + installDriver(args); + return; + } Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false); ProcessBuilder pb = driver.createProcessBuilder(); pb.command().addAll(asList(args)); @@ -40,4 +49,17 @@ public static void main(String[] args) throws IOException, InterruptedException Process process = pb.start(); System.exit(process.waitFor()); } + + private static void installDriver(String[] args) throws IOException, URISyntaxException { + String dir = args.length > 1 ? args[1] : System.getenv(Driver.PLAYWRIGHT_DRIVER_DIR); + if (dir == null) { + System.err.println("Usage: install-driver (or set the " + Driver.PLAYWRIGHT_DRIVER_DIR + + " environment variable)"); + System.exit(1); + return; + } + Path driverDir = Paths.get(dir); + DriverJar.installDriverTo(driverDir); + System.out.println("Installed Playwright driver into " + driverDir.toAbsolutePath()); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/impl/driver/jar/TestInstall.java b/playwright/src/test/java/com/microsoft/playwright/impl/driver/jar/TestInstall.java index 953549bf1..6ee296b47 100644 --- a/playwright/src/test/java/com/microsoft/playwright/impl/driver/jar/TestInstall.java +++ b/playwright/src/test/java/com/microsoft/playwright/impl/driver/jar/TestInstall.java @@ -132,6 +132,30 @@ void canSpecifyPreinstalledNodeJsAsEnv(@TempDir Path tmpDir) throws IOException, } + @Test + void canInstallDriverToDirectoryAndReuseIt(@TempDir Path tmpDir) throws Exception { + Path driverDir = tmpDir.resolve("driver"); + DriverJar.installDriverTo(driverDir); + // The directory is self-contained: the playwright-core package and the Node.js binary. + assertTrue(Files.exists(driverDir.resolve("package").resolve("cli.js"))); + assertTrue(Files.exists(driverDir.resolve(isWindows() ? "node.exe" : "node"))); + + // Pointing playwright.cli.dir at it must reuse it as-is, without extracting to a temp directory. + System.setProperty("playwright.cli.dir", driverDir.toString()); + Driver driver = Driver.createAndInstall(Collections.emptyMap(), false); + assertEquals(driverDir, driver.driverDir()); + + ProcessBuilder pb = driver.createProcessBuilder(); + pb.command().add("--version"); + pb.redirectError(ProcessBuilder.Redirect.INHERIT); + Path out = tmpDir.resolve("out.txt"); + pb.redirectOutput(out.toFile()); + Process p = pb.start(); + assertTrue(p.waitFor(1, TimeUnit.MINUTES), "Timed out waiting for version to be printed"); + String stdout = new String(Files.readAllBytes(out), StandardCharsets.UTF_8); + assertTrue(stdout.contains("Version "), stdout); + } + private static String extractNodeJsToTemp() throws URISyntaxException, IOException { DriverJar auxDriver = new DriverJar(); auxDriver.extractDriverToTempDir(); diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 0f290946f..aa64ef493 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -38,6 +38,10 @@ ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +# Extract the Playwright driver into the image once so the library reuses it instead of unpacking +# it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. +ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver + RUN mkdir /ms-playwright && \ mkdir /tmp/pw-java @@ -45,6 +49,8 @@ COPY . /tmp/pw-java RUN cd /tmp/pw-java && \ mvn install -D skipTests --no-transfer-progress && \ + mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ + -D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \ DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ @@ -61,4 +67,5 @@ RUN cd /tmp/pw-java && \ else \ rm /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstwebrtc.so; \ fi && \ - chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH + chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ + chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 6b8b4bac8..eafb59ba4 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -38,6 +38,10 @@ ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +# Extract the Playwright driver into the image once so the library reuses it instead of unpacking +# it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. +ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver + RUN mkdir /ms-playwright && \ mkdir /tmp/pw-java @@ -45,6 +49,8 @@ COPY . /tmp/pw-java RUN cd /tmp/pw-java && \ mvn install -D skipTests --no-transfer-progress && \ + mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ + -D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \ DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ @@ -52,4 +58,5 @@ RUN cd /tmp/pw-java && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="mark-docker-image '${DOCKER_IMAGE_NAME_TEMPLATE}'" -f playwright/pom.xml --no-transfer-progress && \ rm -rf /tmp/pw-java && \ - chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH + chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ + chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/Dockerfile.resolute b/utils/docker/Dockerfile.resolute index a775c5ca4..64a68de4a 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -38,6 +38,10 @@ ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +# Extract the Playwright driver into the image once so the library reuses it instead of unpacking +# it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. +ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver + RUN mkdir /ms-playwright && \ mkdir /tmp/pw-java @@ -45,6 +49,8 @@ COPY . /tmp/pw-java RUN cd /tmp/pw-java && \ mvn install -D skipTests --no-transfer-progress && \ + mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ + -D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \ DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ @@ -52,4 +58,5 @@ RUN cd /tmp/pw-java && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="mark-docker-image '${DOCKER_IMAGE_NAME_TEMPLATE}'" -f playwright/pom.xml --no-transfer-progress && \ rm -rf /tmp/pw-java && \ - chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH + chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ + chmod -R 777 $PLAYWRIGHT_DRIVER_DIR